saml-test-server 0.0.0-stage → 1.0.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.
Files changed (43) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +134 -2
  3. package/assets/idp-private-key.pem +27 -0
  4. package/assets/idp-public-cert.pem +20 -0
  5. package/dist/cli.js +106 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/index.js +2170 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/types/app.d.ts +16 -0
  10. package/dist/types/cli-program.d.ts +10 -0
  11. package/dist/types/cli.d.ts +2 -0
  12. package/dist/types/default-config.d.ts +12 -0
  13. package/dist/types/index.d.ts +41 -0
  14. package/dist/types/log.d.ts +26 -0
  15. package/dist/types/options.d.ts +111 -0
  16. package/dist/types/profile-mapper.d.ts +10 -0
  17. package/dist/types/session.d.ts +36 -0
  18. package/dist/types/state.d.ts +49 -0
  19. package/dist/types/vendor/saml/encrypt.d.ts +16 -0
  20. package/dist/types/vendor/saml/index.d.ts +7 -0
  21. package/dist/types/vendor/saml/saml20.d.ts +47 -0
  22. package/dist/types/vendor/saml/sign.d.ts +30 -0
  23. package/dist/types/vendor/saml/utils.d.ts +18 -0
  24. package/dist/types/vendor/samlp/auth.d.ts +87 -0
  25. package/dist/types/vendor/samlp/constants.d.ts +30 -0
  26. package/dist/types/vendor/samlp/encoders.d.ts +5 -0
  27. package/dist/types/vendor/samlp/index.d.ts +14 -0
  28. package/dist/types/vendor/samlp/logout.d.ts +66 -0
  29. package/dist/types/vendor/samlp/metadata.d.ts +22 -0
  30. package/dist/types/vendor/samlp/profile-mapper.d.ts +38 -0
  31. package/dist/types/vendor/samlp/session-participants.d.ts +22 -0
  32. package/dist/types/vendor/samlp/signers.d.ts +43 -0
  33. package/dist/types/vendor/samlp/templates.d.ts +52 -0
  34. package/dist/types/vendor/samlp/trim-xml.d.ts +2 -0
  35. package/dist/types/vendor/samlp/utils.d.ts +55 -0
  36. package/dist/types/vendor/samlp/xml.d.ts +7 -0
  37. package/dist/types/views/AutoPostForm.d.ts +12 -0
  38. package/dist/types/views/ErrorPage.d.ts +8 -0
  39. package/dist/types/views/Layout.d.ts +4 -0
  40. package/dist/types/views/SettingsPage.d.ts +5 -0
  41. package/dist/types/views/UserPage.d.ts +20 -0
  42. package/dist/types/views/index.d.ts +5 -0
  43. package/package.json +58 -4
package/dist/index.js ADDED
@@ -0,0 +1,2170 @@
1
+ import { createServer } from "node:https";
2
+ import { homedir, hostname } from "node:os";
3
+ import { serve } from "@hono/node-server";
4
+ import { jsxs, Fragment, jsx } from "hono/jsx/jsx-runtime";
5
+ import { randomBytes, randomInt, X509Certificate, createHash, createVerify, createSign } from "node:crypto";
6
+ import { XMLSerializer, DOMParser } from "@xmldom/xmldom";
7
+ import { Hono } from "hono";
8
+ import { logger } from "hono/logger";
9
+ import pc from "picocolors";
10
+ import xmlFormat from "xml-formatter";
11
+ import { getSignedCookie, deleteCookie, setSignedCookie } from "hono/cookie";
12
+ import { name } from "xml-name-validator";
13
+ import * as xmlenc from "xml-encryption";
14
+ import { SignedXml } from "xml-crypto";
15
+ import xpath from "xpath";
16
+ import { inflateRaw, deflateRaw } from "node:zlib";
17
+ import { promisify } from "node:util";
18
+ import qs from "node:querystring";
19
+ import { raw } from "hono/html";
20
+ import { readFileSync, existsSync } from "node:fs";
21
+ import { resolve, isAbsolute } from "node:path";
22
+ import { fileURLToPath, pathToFileURL } from "node:url";
23
+ const UNDEFINED_VALUE = "None";
24
+ const CERT_OPTIONS = [
25
+ "cert",
26
+ "key",
27
+ "encryptionCert",
28
+ "encryptionPublicKey",
29
+ "httpsPrivateKey",
30
+ "httpsCert"
31
+ ];
32
+ const LOG_LEVELS = ["silent", "error", "info"];
33
+ function isLogLevel(value) {
34
+ return LOG_LEVELS.includes(value);
35
+ }
36
+ function createLogger(level = "info") {
37
+ const noop = () => {
38
+ };
39
+ return {
40
+ log: level === "info" ? (...args) => console.log(...args) : noop,
41
+ error: level === "silent" ? noop : (...args) => console.error(...args)
42
+ };
43
+ }
44
+ function indentLines(text, indent) {
45
+ const pad = " ".repeat(indent);
46
+ return text.replace(/(^|\n)/g, `$1${pad}`);
47
+ }
48
+ function prettyPrintXml(xml, indent = 0) {
49
+ const pretty = xmlFormat(xml, { indentation: " ", collapseContent: true }).replace(
50
+ /<(\/)?((?:[\w]+)(?::))?([\w]+)(.*?)>/g,
51
+ (_m, slash = "", prefix = "", name2, rest) => `<${pc.green(`${slash}${prefix}${pc.bold(name2)}`)}${rest}>`
52
+ ).replace(/ ([\w:]+)="(.+?)"/g, (_m, attr, value) => ` ${pc.white(attr)}=${pc.cyan(`"${value}"`)}`);
53
+ return indent ? indentLines(pretty, indent) : pretty;
54
+ }
55
+ function formatOptionValue(key, value) {
56
+ if (typeof value === "string" && !CERT_OPTIONS.includes(key)) {
57
+ return value;
58
+ }
59
+ if (CERT_OPTIONS.includes(key) && value) {
60
+ return value.toString().replace(/-----.+?-----|\n/g, "").substring(0, 80) + pc.white("…");
61
+ }
62
+ if (!value && value !== false) {
63
+ return UNDEFINED_VALUE;
64
+ }
65
+ if (typeof value === "function") {
66
+ return `${value}`.split("\n")[0].slice(0, -2);
67
+ }
68
+ return JSON.stringify(value);
69
+ }
70
+ function describeRecord(record, indent, format = formatOptionValue) {
71
+ return Object.entries(record).map(([key, value]) => `${" ".repeat(indent)}${key}: ${pc.cyan(format(key, value))}`).join("\n");
72
+ }
73
+ function configBanner(options) {
74
+ const v = (value) => pc.cyan(value === false ? "false" : String(value || UNDEFINED_VALUE));
75
+ return [
76
+ `Listener Port:`,
77
+ ` ${pc.cyan(`${options.host}:${options.port}`)}`,
78
+ `HTTPS Enabled:`,
79
+ ` ${pc.cyan(String(options.https))}`,
80
+ ``,
81
+ pc.bold(`[${pc.yellow("Identity Provider")}]`),
82
+ ``,
83
+ `Issuer URI:`,
84
+ ` ${v(options.issuer)}`,
85
+ `Sign Response Message:`,
86
+ ` ${v(options.signResponse)}`,
87
+ `Encrypt Assertion:`,
88
+ ` ${v(options.encryptAssertion)}`,
89
+ `Authentication Context Class Reference:`,
90
+ ` ${v(options.authnContextClassRef)}`,
91
+ `Authentication Context Declaration:`,
92
+ ` ${v(options.authnContextDecl)}`,
93
+ `Default RelayState:`,
94
+ ` ${v(options.relayState)}`,
95
+ ``,
96
+ pc.bold(`[${pc.yellow("Service Provider")}]`),
97
+ ``,
98
+ `Issuer URI:`,
99
+ ` ${v(options.serviceProviderId)}`,
100
+ `Audience URI:`,
101
+ ` ${v(options.audience)}`,
102
+ `ACS URL:`,
103
+ ` ${v(options.acsUrl)}`,
104
+ `SLO URL:`,
105
+ ` ${v(options.sloUrl)}`,
106
+ `Trust ACS URL in Request:`,
107
+ ` ${pc.cyan(String(!options.disableRequestAcsUrl))}`,
108
+ ``
109
+ ].join("\n");
110
+ }
111
+ function listeningBanner(baseUrl, sloEnabled) {
112
+ const lines = [
113
+ `IdP Metadata URL:`,
114
+ ` ${pc.cyan(`${baseUrl}/metadata`)}`,
115
+ ``,
116
+ `SSO Bindings:`,
117
+ ` urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST`,
118
+ ` => ${pc.cyan(`${baseUrl}/saml/sso`)}`,
119
+ ` urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect`,
120
+ ` => ${pc.cyan(`${baseUrl}/saml/sso`)}`
121
+ ];
122
+ if (sloEnabled) {
123
+ lines.push(
124
+ ``,
125
+ `SLO Bindings:`,
126
+ ` urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST`,
127
+ ` => ${pc.cyan(`${baseUrl}/saml/slo`)}`,
128
+ ` urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect`,
129
+ ` => ${pc.cyan(`${baseUrl}/saml/slo`)}`
130
+ );
131
+ }
132
+ lines.push(``, `IdP server ready at`, ` ${pc.cyan(baseUrl)}`);
133
+ return lines.join("\n");
134
+ }
135
+ function createDefaultConfig() {
136
+ return {
137
+ user: {
138
+ userName: "saml.jackson@example.com",
139
+ nameIdFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
140
+ firstName: "Saml",
141
+ lastName: "Jackson",
142
+ displayName: "saml jackson",
143
+ email: "saml.jackson@example.com",
144
+ mobilePhone: "+1-415-555-5141",
145
+ groups: "Simple IdP Users, West Coast Users, Cloud Users"
146
+ },
147
+ metadata: [
148
+ {
149
+ id: "firstName",
150
+ optional: false,
151
+ displayName: "First Name",
152
+ description: "The given name of the user",
153
+ multiValue: false
154
+ },
155
+ {
156
+ id: "lastName",
157
+ optional: false,
158
+ displayName: "Last Name",
159
+ description: "The surname of the user",
160
+ multiValue: false
161
+ },
162
+ {
163
+ id: "displayName",
164
+ optional: true,
165
+ displayName: "Display Name",
166
+ description: "The display name of the user",
167
+ multiValue: false
168
+ },
169
+ {
170
+ id: "email",
171
+ optional: false,
172
+ displayName: "E-Mail Address",
173
+ description: "The e-mail address of the user",
174
+ multiValue: false
175
+ },
176
+ {
177
+ id: "mobilePhone",
178
+ optional: true,
179
+ displayName: "Mobile Phone",
180
+ description: "The mobile phone of the user",
181
+ multiValue: false
182
+ },
183
+ {
184
+ id: "groups",
185
+ optional: true,
186
+ displayName: "Groups",
187
+ description: "Group memberships of the user",
188
+ multiValue: true
189
+ },
190
+ {
191
+ id: "userType",
192
+ optional: true,
193
+ displayName: "User Type",
194
+ description: "The type of user",
195
+ options: ["Admin", "Editor", "Commenter"]
196
+ }
197
+ ]
198
+ };
199
+ }
200
+ const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
201
+ const DEFAULT_CERT_PATH = resolve(PACKAGE_ROOT, "assets/idp-public-cert.pem");
202
+ const DEFAULT_KEY_PATH = resolve(PACKAGE_ROOT, "assets/idp-private-key.pem");
203
+ const DEFAULT_CONFIG_FILE = "saml-idp/config.js";
204
+ const KEY_CERT_HELP_TEXT = `To generate a key/cert pair for the IdP, run the following command:
205
+
206
+ openssl req -x509 -new -newkey rsa:2048 -nodes \\
207
+ -subj '/C=US/ST=California/L=San Francisco/O=JankyCo/CN=Test Identity Provider' \\
208
+ -keyout idp-private-key.pem \\
209
+ -out idp-public-cert.pem -days 7300`;
210
+ const CRYPT_TYPES = {
211
+ certificate: /-----BEGIN CERTIFICATE-----[^-]*-----END CERTIFICATE-----/,
212
+ // PKCS#1 (RSA PRIVATE KEY) and PKCS#8 (PRIVATE KEY)
213
+ "private key": /-----BEGIN (?:RSA )?PRIVATE KEY-----[^-]*-----END (?:RSA )?PRIVATE KEY-----/,
214
+ "public key": /-----BEGIN (?:RSA )?PUBLIC KEY-----[^-]*-----END (?:RSA )?PUBLIC KEY-----/
215
+ };
216
+ class OptionsError extends Error {
217
+ constructor(message) {
218
+ super(message);
219
+ this.name = "OptionsError";
220
+ }
221
+ }
222
+ function matchesCertType(value, type) {
223
+ return CRYPT_TYPES[type].test(value);
224
+ }
225
+ function resolveFilePath(filePath) {
226
+ if (filePath.startsWith("saml-idp/")) {
227
+ const resolved = resolve(PACKAGE_ROOT, filePath.replace(/^saml-idp\//, ""));
228
+ return existsSync(resolved) ? resolved : void 0;
229
+ }
230
+ if (isAbsolute(filePath) && existsSync(filePath)) return filePath;
231
+ if (filePath.startsWith("~/")) {
232
+ const expanded = resolve(process.env.HOME ?? homedir(), filePath.slice(2));
233
+ return existsSync(expanded) ? expanded : void 0;
234
+ }
235
+ return [process.cwd(), PACKAGE_ROOT].map((base) => resolve(base, filePath)).find((candidate) => existsSync(candidate));
236
+ }
237
+ function coerceCertFile(value, type, description, helpText) {
238
+ const asString = value.toString();
239
+ if (matchesCertType(asString, type)) {
240
+ return asString;
241
+ }
242
+ const filePath = resolveFilePath(asString);
243
+ if (filePath) {
244
+ const content = readFileSync(filePath, "utf8");
245
+ if (matchesCertType(content, type)) return content;
246
+ throw new OptionsError(`Invalid ${description}: "${filePath}" does not contain a valid ${type}`);
247
+ }
248
+ throw new OptionsError(
249
+ `Invalid / missing ${description} - not a valid crypt key/cert or file path` + (helpText ? "\n" + helpText : "")
250
+ );
251
+ }
252
+ async function loadConfigFile(configFile) {
253
+ if (configFile === DEFAULT_CONFIG_FILE) return {};
254
+ const filePath = resolveFilePath(configFile);
255
+ if (!filePath) {
256
+ throw new OptionsError(`SAML attribute config file path "${configFile}" is not a valid path.`);
257
+ }
258
+ try {
259
+ if (filePath.endsWith(".json")) {
260
+ return JSON.parse(readFileSync(filePath, "utf8"));
261
+ }
262
+ const mod = await import(
263
+ /* @vite-ignore */
264
+ pathToFileURL(filePath).href
265
+ );
266
+ return mod.default?.user || mod.default?.metadata ? mod.default : mod;
267
+ } catch (error) {
268
+ throw new OptionsError(
269
+ `Encountered an exception while loading SAML attribute config file "${filePath}".
270
+ ${error}`
271
+ );
272
+ }
273
+ }
274
+ function coerceAuthnContextDecl(value) {
275
+ if (value.trimStart().startsWith("<")) return value;
276
+ const filePath = resolveFilePath(value);
277
+ if (!filePath) {
278
+ throw new OptionsError(`Authentication Context Declaration file "${value}" was not found.`);
279
+ }
280
+ return readFileSync(filePath, "utf8");
281
+ }
282
+ async function resolveOptions(options) {
283
+ if (!options.acsUrl) throw new OptionsError("Missing required option: acsUrl");
284
+ if (!options.audience) throw new OptionsError("Missing required option: audience");
285
+ const encryptAssertion = options.encryptAssertion ?? false;
286
+ if (encryptAssertion && options.encryptionCert === void 0) {
287
+ throw new OptionsError("encryptionCert argument is required for assertion encryption");
288
+ }
289
+ const logLevel = options.logLevel ?? (options.silent ? "silent" : process.env.SAML_IDP_LOG_LEVEL ?? "info");
290
+ if (!isLogLevel(logLevel)) {
291
+ throw new OptionsError(`Invalid logLevel "${logLevel}", expected one of: ${LOG_LEVELS.join(", ")}`);
292
+ }
293
+ const https = options.https ?? false;
294
+ if (https && (options.httpsPrivateKey === void 0 || options.httpsCert === void 0)) {
295
+ throw new OptionsError("httpsPrivateKey and httpsCert arguments are required to enable HTTPS");
296
+ }
297
+ const fileConfig = options.config ? {} : await loadConfigFile(options.configFile ?? DEFAULT_CONFIG_FILE);
298
+ const inlineConfig = options.config ?? fileConfig;
299
+ const defaults = createDefaultConfig();
300
+ const config = {
301
+ // the user is merged with the defaults so that a partial profile still has a NameID
302
+ user: { ...defaults.user, ...inlineConfig.user ?? {} },
303
+ metadata: inlineConfig.metadata ?? defaults.metadata
304
+ };
305
+ return {
306
+ host: options.host ?? "localhost",
307
+ port: options.port ?? 7e3,
308
+ cert: coerceCertFile(
309
+ options.cert ?? DEFAULT_CERT_PATH,
310
+ "certificate",
311
+ "IdP Signature PublicKey Certificate",
312
+ KEY_CERT_HELP_TEXT
313
+ ),
314
+ key: coerceCertFile(
315
+ options.key ?? DEFAULT_KEY_PATH,
316
+ "private key",
317
+ "IdP Signature PrivateKey Certificate",
318
+ KEY_CERT_HELP_TEXT
319
+ ),
320
+ issuer: options.issuer ?? "urn:example:idp",
321
+ acsUrl: options.acsUrl,
322
+ sloUrl: options.sloUrl,
323
+ audience: options.audience,
324
+ serviceProviderId: options.serviceProviderId || options.audience,
325
+ relayState: options.relayState,
326
+ disableRequestAcsUrl: options.disableRequestAcsUrl ?? false,
327
+ encryptAssertion,
328
+ encryptionCert: options.encryptionCert !== void 0 ? coerceCertFile(options.encryptionCert, "certificate", "Encryption cert") : void 0,
329
+ encryptionPublicKey: options.encryptionPublicKey !== void 0 ? coerceCertFile(options.encryptionPublicKey, "public key", "Encryption public key") : void 0,
330
+ httpsPrivateKey: options.httpsPrivateKey !== void 0 ? coerceCertFile(options.httpsPrivateKey, "private key", "Web Server TLS/SSL Private Key") : void 0,
331
+ httpsCert: options.httpsCert !== void 0 ? coerceCertFile(options.httpsCert, "certificate", "Web Server TLS/SSL Certificate") : void 0,
332
+ https,
333
+ signResponse: options.signResponse ?? true,
334
+ rollSession: options.rollSession ?? false,
335
+ authnContextClassRef: options.authnContextClassRef ?? "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
336
+ authnContextDecl: options.authnContextDecl ? coerceAuthnContextDecl(options.authnContextDecl) : void 0,
337
+ logLevel,
338
+ config
339
+ };
340
+ }
341
+ function createSimpleProfileMapper(getMetadata) {
342
+ return {
343
+ get metadata() {
344
+ return getMetadata();
345
+ },
346
+ getClaims(user) {
347
+ const claims = {};
348
+ for (const entry of getMetadata()) {
349
+ const value = user[entry.id];
350
+ claims[entry.id] = entry.multiValue ? (value ?? "").split(",").map((v) => v.trim()).filter((v) => v !== "") : value;
351
+ }
352
+ return Object.keys(claims).length ? claims : void 0;
353
+ },
354
+ getNameIdentifier(user) {
355
+ return {
356
+ nameIdentifier: user.userName,
357
+ nameIdentifierFormat: user.nameIdFormat,
358
+ nameIdentifierNameQualifier: user.nameIdNameQualifier || void 0,
359
+ nameIdentifierSPNameQualifier: user.nameIdSPNameQualifier || void 0,
360
+ nameIdentifierSPProvidedID: user.nameIdSPProvidedID || void 0
361
+ };
362
+ }
363
+ };
364
+ }
365
+ const SESSION_COOKIE = "idp_sid";
366
+ const MAX_AGE_SECONDS = 60 * 60;
367
+ class Session {
368
+ constructor(id, store) {
369
+ this.store = store;
370
+ this.id = id;
371
+ }
372
+ store;
373
+ id;
374
+ /** Arbitrary per-session data. */
375
+ data = {};
376
+ destroyed = false;
377
+ /** Destroys the session. The cookie is cleared at the end of the request. */
378
+ destroy() {
379
+ this.destroyed = true;
380
+ this.store.delete(this.id);
381
+ }
382
+ /** Replaces the session id (and drops the data), like express-session's `regenerate()`. */
383
+ regenerate() {
384
+ this.store.delete(this.id);
385
+ this.id = randomBytes(24).toString("hex");
386
+ this.data = {};
387
+ this.destroyed = false;
388
+ this.store.set(this);
389
+ }
390
+ }
391
+ class SessionStore {
392
+ entries = /* @__PURE__ */ new Map();
393
+ create() {
394
+ const session = new Session(randomBytes(24).toString("hex"), this);
395
+ this.set(session);
396
+ return session;
397
+ }
398
+ get(id) {
399
+ const entry = this.entries.get(id);
400
+ if (!entry) return void 0;
401
+ if (entry.expires < Date.now()) {
402
+ this.entries.delete(id);
403
+ return void 0;
404
+ }
405
+ return entry.session;
406
+ }
407
+ set(session) {
408
+ this.entries.set(session.id, { session, expires: Date.now() + MAX_AGE_SECONDS * 1e3 });
409
+ }
410
+ delete(id) {
411
+ this.entries.delete(id);
412
+ }
413
+ clear() {
414
+ this.entries.clear();
415
+ }
416
+ }
417
+ function sessionMiddleware(store, secret) {
418
+ return async (c, next) => {
419
+ const cookieId = await getSignedCookie(c, secret, SESSION_COOKIE);
420
+ const session = (cookieId ? store.get(cookieId) : void 0) ?? store.create();
421
+ c.set("session", session);
422
+ await next();
423
+ if (session.destroyed) {
424
+ deleteCookie(c, SESSION_COOKIE, { path: "/" });
425
+ return;
426
+ }
427
+ store.set(session);
428
+ await setSignedCookie(c, SESSION_COOKIE, session.id, secret, {
429
+ path: "/",
430
+ httpOnly: true,
431
+ sameSite: "Lax",
432
+ maxAge: MAX_AGE_SECONDS
433
+ });
434
+ };
435
+ }
436
+ function getSessionIndex(session) {
437
+ let hash = 0;
438
+ for (let i = 0; i < session.id.length; i++) {
439
+ hash = (hash << 5) - hash + session.id.charCodeAt(i);
440
+ hash = hash & hash;
441
+ }
442
+ return Math.abs(hash).toString();
443
+ }
444
+ function createLogoutStore(session) {
445
+ const bag = () => session.data._logoutState ??= {};
446
+ return {
447
+ save(state) {
448
+ const handle = randomBytes(16).toString("hex");
449
+ bag()[handle] = state;
450
+ return handle;
451
+ },
452
+ load(handle, options) {
453
+ if (!handle) return void 0;
454
+ const state = bag()[handle];
455
+ if (state && options?.destroy) delete bag()[handle];
456
+ return state;
457
+ },
458
+ update(handle, state) {
459
+ bag()[handle] = state;
460
+ }
461
+ };
462
+ }
463
+ const IDP_PATHS = {
464
+ SSO: "/saml/sso",
465
+ SLO: "/saml/slo",
466
+ METADATA: "/metadata",
467
+ SIGN_IN: "/signin",
468
+ SIGN_OUT: "/signout",
469
+ SETTINGS: "/settings"
470
+ };
471
+ function initialSettings(options) {
472
+ return {
473
+ issuer: options.issuer,
474
+ serviceProviderId: options.serviceProviderId,
475
+ cert: options.cert,
476
+ key: options.key,
477
+ audience: options.audience,
478
+ recipient: options.acsUrl,
479
+ destination: options.acsUrl,
480
+ acsUrl: options.acsUrl,
481
+ sloUrl: options.sloUrl,
482
+ RelayState: options.relayState,
483
+ allowRequestAcsUrl: !options.disableRequestAcsUrl,
484
+ digestAlgorithm: "sha256",
485
+ signatureAlgorithm: "rsa-sha256",
486
+ signResponse: options.signResponse,
487
+ encryptAssertion: options.encryptAssertion,
488
+ encryptionCert: options.encryptionCert,
489
+ encryptionPublicKey: options.encryptionPublicKey,
490
+ encryptionAlgorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc",
491
+ keyEncryptionAlgorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p",
492
+ lifetimeInSeconds: 3600,
493
+ authnContextClassRef: options.authnContextClassRef,
494
+ authnContextDecl: options.authnContextDecl,
495
+ includeAttributeNameFormat: true
496
+ };
497
+ }
498
+ function createState(options) {
499
+ const snapshot = () => ({
500
+ settings: initialSettings(options),
501
+ user: structuredClone(options.config.user),
502
+ metadata: structuredClone(options.config.metadata)
503
+ });
504
+ const state = {
505
+ ...snapshot(),
506
+ reset() {
507
+ Object.assign(state, snapshot());
508
+ }
509
+ };
510
+ return state;
511
+ }
512
+ function pemToCert(pem) {
513
+ const cert = /-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/.exec(pem.toString());
514
+ if (cert) {
515
+ return cert[1].replace(/[\r\n]/g, "");
516
+ }
517
+ return null;
518
+ }
519
+ const UID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
520
+ function uid(len) {
521
+ let out = "";
522
+ for (let i = 0; i < len; i++) {
523
+ out += UID_CHARS[randomInt(0, UID_CHARS.length)];
524
+ }
525
+ return out;
526
+ }
527
+ function removeWhitespace(xml) {
528
+ return xml.replace(/\r\n/g, "").replace(/\n/g, "").replace(/>(\s*)</g, "><").trim();
529
+ }
530
+ function fixPemFormatting(pem) {
531
+ const entries = pem.toString().matchAll(/([-]{5}[^-\r\n]+[-]{5})([^-]*)([-]{5}[^-\r\n]+[-]{5})/g);
532
+ let fixed = "";
533
+ for (const parts of entries) {
534
+ fixed += `${parts[1]}
535
+ ${parts[2].replace(/[\r\n]/g, "")}
536
+ ${parts[3]}
537
+ `;
538
+ }
539
+ if (fixed.length === 0) {
540
+ return pem;
541
+ }
542
+ return Buffer.from(fixed);
543
+ }
544
+ function formatInstant(date) {
545
+ return date.toISOString();
546
+ }
547
+ function escapeXml(value) {
548
+ return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
549
+ }
550
+ const DEFAULT_ENCRYPTION_ALGORITHM = "http://www.w3.org/2001/04/xmlenc#aes256-cbc";
551
+ const DEFAULT_KEY_ENCRYPTION_ALGORITHM = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p";
552
+ function derivePublicKey(cert) {
553
+ return new X509Certificate(cert).publicKey.export({ type: "spki", format: "pem" }).toString();
554
+ }
555
+ function encryptOnce(xml, options) {
556
+ return new Promise((resolve2, reject) => {
557
+ xmlenc.encrypt(xml, options, (err, encrypted) => err ? reject(err) : resolve2(encrypted));
558
+ });
559
+ }
560
+ async function encryptXml(xml, options) {
561
+ const encryptOptions = {
562
+ rsa_pub: options.encryptionPublicKey ?? derivePublicKey(options.encryptionCert),
563
+ pem: options.encryptionCert,
564
+ encryptionAlgorithm: options.encryptionAlgorithm ?? DEFAULT_ENCRYPTION_ALGORITHM,
565
+ keyEncryptionAlgorithm: options.keyEncryptionAlgorithm ?? DEFAULT_KEY_ENCRYPTION_ALGORITHM
566
+ };
567
+ try {
568
+ return removeWhitespace(await encryptOnce(xml, encryptOptions));
569
+ } catch {
570
+ const retried = await encryptOnce(xml, {
571
+ ...encryptOptions,
572
+ rsa_pub: fixPemFormatting(encryptOptions.rsa_pub),
573
+ pem: fixPemFormatting(encryptOptions.pem)
574
+ });
575
+ return removeWhitespace(retried);
576
+ }
577
+ }
578
+ const SIGNATURE_ALGORITHMS = {
579
+ "rsa-sha256": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
580
+ "rsa-sha1": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
581
+ };
582
+ const DIGEST_ALGORITHMS = {
583
+ sha256: "http://www.w3.org/2001/04/xmlenc#sha256",
584
+ sha1: "http://www.w3.org/2000/09/xmldsig#sha1"
585
+ };
586
+ const ENVELOPED_SIGNATURE = "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
587
+ const EXC_C14N = "http://www.w3.org/2001/10/xml-exc-c14n#";
588
+ function signXml$1(xml, options) {
589
+ if (!options.key) throw new Error("Expect a private key in pem format");
590
+ if (!options.cert) throw new Error("Expect a public key cert in pem format");
591
+ const cert = pemToCert(options.cert);
592
+ if (!cert) throw new Error("Expect a public key cert in pem format");
593
+ const sign2 = (key) => {
594
+ const sig = new SignedXml({
595
+ privateKey: key,
596
+ signatureAlgorithm: SIGNATURE_ALGORITHMS[options.signatureAlgorithm ?? "rsa-sha256"],
597
+ canonicalizationAlgorithm: EXC_C14N,
598
+ idAttribute: options.signatureIdAttribute,
599
+ getKeyInfoContent: ({ prefix } = {}) => {
600
+ const p = prefix ? `${prefix}:` : "";
601
+ return `<${p}X509Data><${p}X509Certificate>${cert}</${p}X509Certificate></${p}X509Data>`;
602
+ }
603
+ });
604
+ sig.addReference({
605
+ xpath: options.xpathToSign,
606
+ transforms: [ENVELOPED_SIGNATURE, EXC_C14N],
607
+ digestAlgorithm: DIGEST_ALGORITHMS[options.digestAlgorithm ?? "sha256"]
608
+ });
609
+ sig.computeSignature(xml, {
610
+ location: { reference: options.xpathToNodeBeforeSignature, action: "after" },
611
+ prefix: options.signatureNamespacePrefix ?? ""
612
+ });
613
+ return sig.getSignedXml();
614
+ };
615
+ try {
616
+ return sign2(options.key);
617
+ } catch {
618
+ return sign2(fixPemFormatting(options.key));
619
+ }
620
+ }
621
+ function unsigned(xml) {
622
+ return removeWhitespace(xml);
623
+ }
624
+ const SAML_ASSERTION_NS = "urn:oasis:names:tc:SAML:2.0:assertion";
625
+ const ASSERTION_TEMPLATE = `<saml:Assertion xmlns:saml="${SAML_ASSERTION_NS}" Version="2.0" ID="" IssueInstant="">
626
+ <saml:Issuer></saml:Issuer>
627
+ <saml:Subject>
628
+ <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" />
629
+ <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
630
+ <saml:SubjectConfirmationData />
631
+ </saml:SubjectConfirmation>
632
+ </saml:Subject>
633
+ <saml:Conditions />
634
+ <saml:AuthnStatement AuthnInstant="">
635
+ <saml:AuthnContext>
636
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
637
+ </saml:AuthnContext>
638
+ </saml:AuthnStatement>
639
+ </saml:Assertion>`;
640
+ function getAttributeType(value) {
641
+ switch (typeof value) {
642
+ case "string":
643
+ return "xs:string";
644
+ case "boolean":
645
+ return "xs:boolean";
646
+ case "number":
647
+ return "xs:double";
648
+ default:
649
+ return "xs:anyType";
650
+ }
651
+ }
652
+ const URI_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:\S+$/;
653
+ function getNameFormat(name$1) {
654
+ if (URI_RE.test(name$1)) {
655
+ return "urn:oasis:names:tc:SAML:2.0:attrname-format:uri";
656
+ }
657
+ if (name(name$1)) {
658
+ return "urn:oasis:names:tc:SAML:2.0:attrname-format:basic";
659
+ }
660
+ return "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified";
661
+ }
662
+ function buildAssertionDocument(options) {
663
+ const doc = new DOMParser().parseFromString(ASSERTION_TEMPLATE, "text/xml");
664
+ const root = doc.documentElement;
665
+ root.setAttribute("ID", `_${options.uid || uid(32)}`);
666
+ if (options.issuer) {
667
+ root.getElementsByTagName("saml:Issuer")[0].textContent = options.issuer;
668
+ }
669
+ const now = /* @__PURE__ */ new Date();
670
+ root.setAttribute("IssueInstant", formatInstant(now));
671
+ const conditions = root.getElementsByTagName("saml:Conditions")[0];
672
+ const confirmationData = root.getElementsByTagName("saml:SubjectConfirmationData")[0];
673
+ if (options.lifetimeInSeconds) {
674
+ const notOnOrAfter = formatInstant(new Date(now.getTime() + options.lifetimeInSeconds * 1e3));
675
+ conditions.setAttribute("NotBefore", formatInstant(now));
676
+ conditions.setAttribute("NotOnOrAfter", notOnOrAfter);
677
+ confirmationData.setAttribute("NotOnOrAfter", notOnOrAfter);
678
+ }
679
+ if (options.audiences) {
680
+ const audienceRestriction = doc.createElementNS(SAML_ASSERTION_NS, "saml:AudienceRestriction");
681
+ const audiences = Array.isArray(options.audiences) ? options.audiences : [options.audiences];
682
+ for (const audience of audiences) {
683
+ const element = doc.createElementNS(SAML_ASSERTION_NS, "saml:Audience");
684
+ element.textContent = audience;
685
+ audienceRestriction.appendChild(element);
686
+ }
687
+ conditions.appendChild(audienceRestriction);
688
+ }
689
+ if (options.recipient) confirmationData.setAttribute("Recipient", options.recipient);
690
+ if (options.inResponseTo) confirmationData.setAttribute("InResponseTo", options.inResponseTo);
691
+ const typedAttributes = options.typedAttributes ?? true;
692
+ const includeAttributeNameFormat = options.includeAttributeNameFormat ?? true;
693
+ if (options.attributes) {
694
+ const statement = doc.createElementNS(SAML_ASSERTION_NS, "saml:AttributeStatement");
695
+ statement.setAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema");
696
+ statement.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
697
+ root.appendChild(statement);
698
+ for (const [prop, raw2] of Object.entries(options.attributes)) {
699
+ if (typeof raw2 === "undefined") continue;
700
+ const attributeElement = doc.createElementNS(SAML_ASSERTION_NS, "saml:Attribute");
701
+ attributeElement.setAttribute("Name", prop);
702
+ if (includeAttributeNameFormat) {
703
+ attributeElement.setAttribute("NameFormat", getNameFormat(prop));
704
+ }
705
+ const values = (Array.isArray(raw2) ? raw2 : [raw2]).filter((v) => typeof v !== "undefined");
706
+ for (const value of values) {
707
+ const valueElement = doc.createElementNS(SAML_ASSERTION_NS, "saml:AttributeValue");
708
+ valueElement.setAttribute("xsi:type", typedAttributes ? getAttributeType(value) : "xs:anyType");
709
+ valueElement.textContent = String(value);
710
+ attributeElement.appendChild(valueElement);
711
+ }
712
+ if (values.length > 0) {
713
+ statement.appendChild(attributeElement);
714
+ }
715
+ }
716
+ }
717
+ const authnStatement = root.getElementsByTagName("saml:AuthnStatement")[0];
718
+ authnStatement.setAttribute("AuthnInstant", formatInstant(now));
719
+ if (options.sessionIndex) {
720
+ authnStatement.setAttribute("SessionIndex", options.sessionIndex);
721
+ }
722
+ const nameID = root.getElementsByTagNameNS(SAML_ASSERTION_NS, "NameID")[0];
723
+ if (options.nameIdentifier) nameID.textContent = options.nameIdentifier;
724
+ if (options.nameIdentifierFormat) nameID.setAttribute("Format", options.nameIdentifierFormat);
725
+ if (options.nameIdentifierNameQualifier) {
726
+ nameID.setAttribute("NameQualifier", options.nameIdentifierNameQualifier);
727
+ }
728
+ if (options.nameIdentifierSPNameQualifier) {
729
+ nameID.setAttribute("SPNameQualifier", options.nameIdentifierSPNameQualifier);
730
+ }
731
+ if (options.nameIdentifierSPProvidedID) {
732
+ nameID.setAttribute("SPProvidedID", options.nameIdentifierSPProvidedID);
733
+ }
734
+ if (options.authnContextClassRef) {
735
+ root.getElementsByTagName("saml:AuthnContextClassRef")[0].textContent = options.authnContextClassRef;
736
+ }
737
+ if (typeof options.transformAssertion === "function") {
738
+ options.transformAssertion(doc);
739
+ }
740
+ return doc;
741
+ }
742
+ async function createAssertion(options) {
743
+ const doc = buildAssertionDocument(options);
744
+ const xml = removeWhitespace(new XMLSerializer().serializeToString(doc));
745
+ const signed = options.signAssertion === false ? unsigned(xml) : signXml$1(xml, {
746
+ key: options.key,
747
+ cert: options.cert,
748
+ signatureAlgorithm: options.signatureAlgorithm,
749
+ digestAlgorithm: options.digestAlgorithm,
750
+ signatureNamespacePrefix: options.signatureNamespacePrefix,
751
+ xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']",
752
+ xpathToSign: "//*[local-name(.)='Assertion']",
753
+ signatureIdAttribute: "ID"
754
+ });
755
+ if (!options.encryptionCert) {
756
+ return signed;
757
+ }
758
+ const encryptOptions = {
759
+ encryptionCert: options.encryptionCert,
760
+ encryptionPublicKey: options.encryptionPublicKey,
761
+ encryptionAlgorithm: options.encryptionAlgorithm,
762
+ keyEncryptionAlgorithm: options.keyEncryptionAlgorithm
763
+ };
764
+ const encrypted = await encryptXml(signed, encryptOptions);
765
+ return removeWhitespace(
766
+ `<saml:EncryptedAssertion xmlns:saml="${SAML_ASSERTION_NS}">${encrypted}</saml:EncryptedAssertion>`
767
+ );
768
+ }
769
+ const BINDINGS = {
770
+ HTTP_REDIRECT: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
771
+ };
772
+ const STATUS = {
773
+ SUCCESS: "urn:oasis:names:tc:SAML:2.0:status:Success",
774
+ PARTIAL_LOGOUT: "urn:oasis:names:tc:SAML:2.0:status:PartialLogout"
775
+ };
776
+ const ELEMENTS = {
777
+ LOGOUT_REQUEST: {
778
+ PROP: "SAMLRequest",
779
+ SIGNATURE_VALIDATION_PATH: "//*[local-name(.)='LogoutRequest']/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
780
+ SIGNATURE_LOCATION_PATH: "//*[local-name(.)='LogoutRequest' and namespace-uri(.)='urn:oasis:names:tc:SAML:2.0:protocol']",
781
+ ISSUER_PATH: "//*[local-name(.)='Issuer' and namespace-uri(.)='urn:oasis:names:tc:SAML:2.0:assertion']/text()",
782
+ SESSION_INDEX_PATH: "//*[local-name(.)='SessionIndex' and namespace-uri(.)='urn:oasis:names:tc:SAML:2.0:protocol']/text()",
783
+ NAME_ID: "//*[local-name(.)='NameID' and namespace-uri(.)='urn:oasis:names:tc:SAML:2.0:assertion']/text()"
784
+ },
785
+ LOGOUT_RESPONSE: {
786
+ PROP: "SAMLResponse",
787
+ SIGNATURE_VALIDATION_PATH: "//*[local-name(.)='LogoutResponse']/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
788
+ SIGNATURE_LOCATION_PATH: "//*[local-name(.)='LogoutResponse' and namespace-uri(.)='urn:oasis:names:tc:SAML:2.0:protocol']"
789
+ },
790
+ RESPONSE: {
791
+ PROP: "SAMLResponse",
792
+ SIGNATURE_LOCATION_PATH: "//*[local-name(.)='Response' and namespace-uri(.)='urn:oasis:names:tc:SAML:2.0:protocol']"
793
+ },
794
+ AUTHN_REQUEST: {
795
+ PROP: "SAMLRequest",
796
+ SIGNATURE_VALIDATION_PATH: "//*[local-name(.)='AuthnRequest']/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
797
+ AUTHN_CONTEXT_CLASS_REF_PATH: "//*[local-name(.)='AuthnContextClassRef']/text()"
798
+ }
799
+ };
800
+ const ALGORITHMS = {
801
+ SIGNATURE: {
802
+ "rsa-sha256": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
803
+ "rsa-sha1": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
804
+ }
805
+ };
806
+ function removeHeaders(cert) {
807
+ const pem = /-----BEGIN (\w*)-----([^-]*)-----END (\w*)-----/g.exec(cert.toString());
808
+ if (pem && pem.length > 0) {
809
+ return pem[2].replace(/[\r\n]/g, "");
810
+ }
811
+ return null;
812
+ }
813
+ function calculateThumbprint(base64Cert) {
814
+ return createHash("sha1").update(Buffer.from(base64Cert.replace(/\s/g, ""), "base64")).digest("hex").toUpperCase();
815
+ }
816
+ const fm = {
817
+ nameIdentifier: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
818
+ email: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
819
+ name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
820
+ givenname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
821
+ surname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
822
+ };
823
+ const DONT_REMAP = ["emails", "displayName", "name", "id", "_json"];
824
+ const passportProfileMapper = {
825
+ metadata: [
826
+ { id: fm.email, optional: true, displayName: "E-Mail Address", description: "The e-mail address of the user" },
827
+ { id: fm.givenname, optional: true, displayName: "Given Name", description: "The given name of the user" },
828
+ { id: fm.name, optional: true, displayName: "Name", description: "The unique name of the user" },
829
+ { id: fm.surname, optional: true, displayName: "Surname", description: "The surname of the user" },
830
+ { id: fm.nameIdentifier, optional: true, displayName: "Name ID", description: "The SAML name identifier of the user" }
831
+ ],
832
+ getClaims(pu) {
833
+ const claims = {};
834
+ claims[fm.nameIdentifier] = pu.id;
835
+ claims[fm.email] = pu.emails?.[0]?.value;
836
+ claims[fm.name] = pu.displayName;
837
+ claims[fm.givenname] = pu.name?.givenName;
838
+ claims[fm.surname] = pu.name?.familyName;
839
+ for (const key of Object.keys(pu)) {
840
+ if (!DONT_REMAP.includes(key)) {
841
+ claims["http://schemas.passportjs.com/" + key] = pu[key];
842
+ }
843
+ }
844
+ return claims;
845
+ },
846
+ getNameIdentifier(pu) {
847
+ const claims = this.getClaims(pu) ?? {};
848
+ return {
849
+ nameIdentifier: claims[fm.nameIdentifier] || claims[fm.name] || claims[fm.email]
850
+ };
851
+ }
852
+ };
853
+ function parseXml(xml) {
854
+ return new DOMParser({
855
+ onError: (level, msg) => {
856
+ if (level === "fatalError") throw new Error(msg);
857
+ }
858
+ }).parseFromString(xml, "text/xml");
859
+ }
860
+ function selectNodes(expression, node) {
861
+ const result = xpath.select(expression, node);
862
+ return Array.isArray(result) ? result : [];
863
+ }
864
+ const DEFAULT_SIG_ALG = "rsa-sha256";
865
+ const DEFAULT_DIGEST_ALG = "sha256";
866
+ const DSIG_NS = "http://www.w3.org/2000/09/xmldsig#";
867
+ function getSigAlg(options) {
868
+ return ALGORITHMS.SIGNATURE[options.signatureAlgorithm ?? DEFAULT_SIG_ALG];
869
+ }
870
+ function signXml(options, xml) {
871
+ return signXml$1(xml, {
872
+ key: options.key,
873
+ cert: options.cert,
874
+ signatureAlgorithm: options.signatureAlgorithm ?? DEFAULT_SIG_ALG,
875
+ digestAlgorithm: options.digestAlgorithm ?? DEFAULT_DIGEST_ALG,
876
+ xpathToSign: options.reference ?? ELEMENTS.LOGOUT_REQUEST.SIGNATURE_LOCATION_PATH,
877
+ xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']",
878
+ signatureIdAttribute: "ID"
879
+ });
880
+ }
881
+ function certToPEM(cert) {
882
+ const value = cert.toString();
883
+ if (/-----BEGIN CERTIFICATE-----/.test(value)) {
884
+ return value;
885
+ }
886
+ const body = value.replace(/\s/g, "").match(/.{1,64}/g)?.join("\n") ?? "";
887
+ return `-----BEGIN CERTIFICATE-----
888
+ ${body}
889
+ -----END CERTIFICATE-----
890
+ `;
891
+ }
892
+ function validateXmlEmbeddedSignature(xml, xmlString, options) {
893
+ const signature = selectNodes(
894
+ "/*/*[local-name(.)='Signature' and namespace-uri(.)='" + DSIG_NS + "']",
895
+ xml
896
+ )[0];
897
+ if (!signature) {
898
+ return ["Signature is missing"];
899
+ }
900
+ const thumbprints = options.thumbprints ? Array.isArray(options.thumbprints) ? options.thumbprints : [options.thumbprints] : [];
901
+ let calculatedThumbprint = "";
902
+ const sig = new SignedXml({
903
+ publicCert: options.signingCert ? certToPEM(options.signingCert) : void 0,
904
+ getCertFromKeyInfo: (keyInfo) => {
905
+ if (thumbprints.length === 0 || !keyInfo) return null;
906
+ const certNode = selectNodes(".//*[local-name(.)='X509Certificate']", keyInfo)[0];
907
+ const base64 = certNode?.textContent?.replace(/\s/g, "");
908
+ if (!base64) return null;
909
+ calculatedThumbprint = calculateThumbprint(base64);
910
+ return certToPEM(base64);
911
+ }
912
+ });
913
+ if (!options.signingCert && thumbprints.length === 0) {
914
+ return ["options.signingCert or options.thumbprints must be specified to validate the signature"];
915
+ }
916
+ try {
917
+ sig.loadSignature(signature);
918
+ if (!sig.checkSignature(xmlString)) {
919
+ return sig.getSignedReferences().length === 0 ? ["Invalid signature"] : ["Invalid signature reference"];
920
+ }
921
+ } catch (e) {
922
+ return [e instanceof Error ? e.message : String(e)];
923
+ }
924
+ if (thumbprints.length > 0 && !options.signingCert) {
925
+ const valid = thumbprints.some((t) => calculatedThumbprint === t.replace(/:/g, "").toUpperCase());
926
+ if (!valid) {
927
+ return [
928
+ `Invalid thumbprint (configured: ${thumbprints.join(", ").toUpperCase()}. calculated: ${calculatedThumbprint})`
929
+ ];
930
+ }
931
+ }
932
+ return [];
933
+ }
934
+ function sign(options, content) {
935
+ const signer = createSign((options.signatureAlgorithm ?? DEFAULT_SIG_ALG).toUpperCase());
936
+ signer.update(content);
937
+ return signer.sign(options.key, "base64");
938
+ }
939
+ function isValidContentAndSignature(content, signature, options) {
940
+ const verifier = createVerify(options.signatureAlgorithm.split("#")[1].toUpperCase());
941
+ verifier.update(content);
942
+ return verifier.verify(certToPEM(options.signingCert), signature, "base64");
943
+ }
944
+ function samlResponse(data) {
945
+ return `<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="${escapeXml(data.id)}"` + (data.inResponseTo ? ` InResponseTo="${escapeXml(data.inResponseTo)}"` : "") + ` Version="2.0" IssueInstant="${escapeXml(data.instant)}"` + (data.destination ? ` Destination="${escapeXml(data.destination)}"` : "") + `><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${escapeXml(data.issuer)}</saml:Issuer><samlp:Status><samlp:StatusCode Value="${escapeXml(data.samlStatusCode)}"/>` + (data.samlStatusMessage ? `<samlp:StatusMessage Value="${escapeXml(data.samlStatusMessage)}"/>` : "") + `</samlp:Status>` + (data.assertion ?? "") + `</samlp:Response>`;
946
+ }
947
+ function logoutResponse(data) {
948
+ return `<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="${escapeXml(data.id)}"` + (data.inResponseTo ? ` InResponseTo="${escapeXml(data.inResponseTo)}"` : "") + ` Version="2.0" IssueInstant="${escapeXml(data.instant)}"` + (data.destination ? ` Destination="${escapeXml(data.destination)}"` : "") + `><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">${escapeXml(data.issuer)}</saml:Issuer><samlp:Status><samlp:StatusCode Value="${escapeXml(data.samlStatusCode)}"/>` + (data.samlStatusMessage ? `<samlp:StatusMessage>${escapeXml(data.samlStatusMessage)}</samlp:StatusMessage>` : "") + `</samlp:Status></samlp:LogoutResponse>`;
949
+ }
950
+ function logoutRequest(data) {
951
+ const nameId = data.NameID;
952
+ return `<samlp:LogoutRequest ID="_${escapeXml(data.ID)}" Version="2.0" IssueInstant="${escapeXml(data.IssueInstant)}"` + (data.Destination ? ` Destination="${escapeXml(data.Destination)}"` : "") + ` Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">${escapeXml(data.Issuer)}</Issuer><NameID xmlns="urn:oasis:names:tc:SAML:2.0:assertion"` + (nameId.NameQualifier ? ` NameQualifier="${escapeXml(nameId.NameQualifier)}"` : "") + (nameId.SPNameQualifier ? ` SPNameQualifier="${escapeXml(nameId.SPNameQualifier)}"` : "") + (nameId.Format ? ` Format="${escapeXml(nameId.Format)}"` : "") + (nameId.SPProvidedID ? ` SPProvidedID="${escapeXml(nameId.SPProvidedID)}"` : "") + `>${escapeXml(nameId.value)}</NameID>` + (data.SessionIndex ? `<samlp:SessionIndex>${escapeXml(data.SessionIndex)}</samlp:SessionIndex>` : "") + `</samlp:LogoutRequest>`;
953
+ }
954
+ function metadata(data) {
955
+ const lines = [
956
+ `<EntityDescriptor entityID="${escapeXml(data.issuer)}" xmlns="urn:oasis:names:tc:SAML:2.0:metadata">`,
957
+ ` <IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">`,
958
+ ` <KeyDescriptor use="signing">`,
959
+ ` <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">`,
960
+ ` <X509Data>`,
961
+ ` <X509Certificate>${escapeXml(data.pem)}</X509Certificate>`,
962
+ ` </X509Data>`,
963
+ ` </KeyInfo>`,
964
+ ` </KeyDescriptor>`
965
+ ];
966
+ if (data.logoutEndpoints.redirect) {
967
+ lines.push(
968
+ ` <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="${escapeXml(data.logoutEndpoints.redirect)}"/>`
969
+ );
970
+ }
971
+ if (data.logoutEndpoints.post) {
972
+ lines.push(
973
+ ` <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${escapeXml(data.logoutEndpoints.post)}"/>`
974
+ );
975
+ }
976
+ lines.push(
977
+ ` <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>`,
978
+ ` <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>`,
979
+ ` <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>`,
980
+ ` <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="${escapeXml(data.redirectEndpoint)}"/>`,
981
+ ` <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${escapeXml(data.postEndpoint)}"/>`
982
+ );
983
+ for (const ct of data.claimTypes) {
984
+ lines.push(
985
+ ` <Attribute Name="${escapeXml(ct.id)}" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="${escapeXml(ct.displayName)}" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"/>`
986
+ );
987
+ }
988
+ lines.push(` </IDPSSODescriptor>`, `</EntityDescriptor>`);
989
+ return lines.join("\n");
990
+ }
991
+ const inflateRawAsync$1 = promisify(inflateRaw);
992
+ const SIGNATURE_ALGORITHM_NAMES = {
993
+ "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": "rsa-sha256",
994
+ "http://www.w3.org/2000/09/xmldsig#rsa-sha1": "rsa-sha1"
995
+ };
996
+ function generateUniqueID() {
997
+ return randomBytes(10).toString("hex");
998
+ }
999
+ function formatXmlDateTime(date) {
1000
+ return date.toISOString();
1001
+ }
1002
+ function generateInstant() {
1003
+ return formatXmlDateTime(/* @__PURE__ */ new Date());
1004
+ }
1005
+ function appendQueryString(initialUrl, query) {
1006
+ const parsed = new URL(initialUrl);
1007
+ for (const [key, value] of Object.entries(query)) {
1008
+ parsed.searchParams.set(key, value);
1009
+ }
1010
+ return parsed.toString();
1011
+ }
1012
+ function isXmlBuffer(input) {
1013
+ return input[0] === 60;
1014
+ }
1015
+ async function parseSamlRequest(input, samlRequest, type, options = {}) {
1016
+ if (!samlRequest) return void 0;
1017
+ const raw2 = Buffer.from(samlRequest, "base64");
1018
+ let xmlString;
1019
+ if (isXmlBuffer(raw2)) {
1020
+ xmlString = raw2.toString();
1021
+ } else {
1022
+ xmlString = (await inflateRawAsync$1(raw2)).toString();
1023
+ }
1024
+ let xml;
1025
+ try {
1026
+ xml = parseXml(xmlString);
1027
+ } catch (e) {
1028
+ throw new Error(e instanceof Error ? e.message : String(e));
1029
+ }
1030
+ if (!xml || !xml.documentElement) {
1031
+ throw new Error("Invalid SAML Request");
1032
+ }
1033
+ const credentials = await configureSigningCredentials(xml, type, options);
1034
+ if (!credentials) return xml;
1035
+ validateSignature(input, type, xml, xmlString, {
1036
+ signingCert: credentials.cert,
1037
+ thumbprints: credentials.thumbprints,
1038
+ deflate: !!input.query.Signature,
1039
+ relayState: options.relayState,
1040
+ signaturePath: options.signaturePath
1041
+ });
1042
+ return xml;
1043
+ }
1044
+ async function configureSigningCredentials(xml, type, options) {
1045
+ if (options.signingCert || options.thumbprints && options.thumbprints.length > 0) {
1046
+ return { cert: options.signingCert, thumbprints: options.thumbprints };
1047
+ }
1048
+ if (!options.getCredentials) return void 0;
1049
+ const descriptor = ELEMENTS[type];
1050
+ const issuer = descriptor.ISSUER_PATH ? selectNodes(descriptor.ISSUER_PATH, xml)[0]?.textContent ?? void 0 : void 0;
1051
+ let sessionIndices;
1052
+ if (descriptor.SESSION_INDEX_PATH) {
1053
+ const nodes = selectNodes(descriptor.SESSION_INDEX_PATH, xml);
1054
+ if (nodes.length > 0) sessionIndices = nodes.map((n) => n.textContent ?? "");
1055
+ }
1056
+ let nameId;
1057
+ if (descriptor.NAME_ID) {
1058
+ nameId = selectNodes(descriptor.NAME_ID, xml)[0]?.textContent ?? void 0;
1059
+ }
1060
+ if (!issuer && !(sessionIndices && sessionIndices.length) && !nameId) {
1061
+ return void 0;
1062
+ }
1063
+ let credentials;
1064
+ try {
1065
+ credentials = await options.getCredentials(issuer, sessionIndices, nameId);
1066
+ } catch {
1067
+ return void 0;
1068
+ }
1069
+ if (!credentials) return void 0;
1070
+ return credentials;
1071
+ }
1072
+ function validateSignature(input, elementType, xml, xmlString, options) {
1073
+ const descriptor = ELEMENTS[elementType];
1074
+ const prop = descriptor.PROP;
1075
+ const isRequestSigned = !options.deflate ? selectNodes(options.signaturePath ?? descriptor.SIGNATURE_VALIDATION_PATH, xml).length > 0 : !!input.query.Signature;
1076
+ if (isRequestSigned) {
1077
+ if (input.body[prop] || !options.deflate) {
1078
+ const errors = validateXmlEmbeddedSignature(xml, xmlString, options);
1079
+ if (errors.length > 0) {
1080
+ throw new Error("Signature check errors: " + errors.join("; "));
1081
+ }
1082
+ } else {
1083
+ const signedContent = {
1084
+ [prop]: input.query[prop],
1085
+ RelayState: input.query.RelayState || options.relayState,
1086
+ SigAlg: input.query.SigAlg
1087
+ };
1088
+ if (!signedContent.RelayState) {
1089
+ delete signedContent.RelayState;
1090
+ }
1091
+ if (!signedContent.SigAlg) {
1092
+ throw new Error("Signature Algorithm is missing");
1093
+ }
1094
+ if (!SIGNATURE_ALGORITHM_NAMES[signedContent.SigAlg]) {
1095
+ throw new Error(
1096
+ "Invalid signature algorithm. Supported algorithms are http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 and http://www.w3.org/2000/09/xmldsig#rsa-sha1"
1097
+ );
1098
+ }
1099
+ if (!options.signingCert) {
1100
+ throw new Error("options.signingCert must be specified to validate a detached signature");
1101
+ }
1102
+ const valid = isValidContentAndSignature(
1103
+ qs.stringify(signedContent),
1104
+ input.query.Signature,
1105
+ { signingCert: options.signingCert, signatureAlgorithm: signedContent.SigAlg }
1106
+ );
1107
+ if (!valid) {
1108
+ throw new Error(
1109
+ "Signature check errors: The signature provided (" + input.query.Signature + ") does not match the one calculated"
1110
+ );
1111
+ }
1112
+ }
1113
+ } else if (options.signingCert) {
1114
+ throw new Error(prop + " message MUST be signed when using an asynchronous binding (POST or Redirect)");
1115
+ }
1116
+ }
1117
+ const ISSUER_XPATH = "//*[local-name(.)='Issuer' and namespace-uri(.)='urn:oasis:names:tc:SAML:2.0:assertion']/text()";
1118
+ class SamlpError extends Error {
1119
+ status;
1120
+ context;
1121
+ constructor(message, status = 500, context) {
1122
+ super(message);
1123
+ this.name = "SamlpError";
1124
+ this.status = status;
1125
+ this.context = context;
1126
+ }
1127
+ }
1128
+ function buildSamlResponse(options) {
1129
+ let response = samlResponse({
1130
+ id: "_" + generateUniqueID(),
1131
+ instant: generateInstant(),
1132
+ destination: options.destination || options.audience,
1133
+ inResponseTo: options.inResponseTo,
1134
+ issuer: options.issuer,
1135
+ samlStatusCode: options.samlStatusCode ?? STATUS.SUCCESS,
1136
+ samlStatusMessage: options.samlStatusMessage,
1137
+ assertion: options.samlAssertion || ""
1138
+ });
1139
+ if (options.signResponse) {
1140
+ response = signXml(
1141
+ {
1142
+ key: options.key,
1143
+ cert: options.cert,
1144
+ signatureAlgorithm: options.signatureAlgorithm,
1145
+ digestAlgorithm: options.digestAlgorithm,
1146
+ reference: ELEMENTS.RESPONSE.SIGNATURE_LOCATION_PATH
1147
+ },
1148
+ response
1149
+ );
1150
+ }
1151
+ return response;
1152
+ }
1153
+ function nameIdentifierNotFoundErrorMessage(options) {
1154
+ const probes = Array.isArray(options.nameIdentifierProbes) ? options.nameIdentifierProbes.join(", ") : "";
1155
+ return "No attribute was found to generate the nameIdentifier. We tried with: " + probes;
1156
+ }
1157
+ async function createSamlResponse(samlConfig, user) {
1158
+ const options = {
1159
+ ...samlConfig,
1160
+ profileMapper: samlConfig.profileMapper ?? passportProfileMapper,
1161
+ signAssertion: typeof samlConfig.signAssertion !== "boolean" || samlConfig.signAssertion,
1162
+ signatureNamespacePrefix: typeof samlConfig.signatureNamespacePrefix === "string" ? samlConfig.signatureNamespacePrefix : ""
1163
+ };
1164
+ const mapper = options.profileMapper;
1165
+ const claims = mapper.getClaims(user);
1166
+ const ni = mapper.getNameIdentifier(user);
1167
+ if (!ni || !ni.nameIdentifier) {
1168
+ throw new SamlpError(nameIdentifierNotFoundErrorMessage(options), 500, { user });
1169
+ }
1170
+ const samlAssertion = await createAssertion({
1171
+ signatureAlgorithm: options.signatureAlgorithm,
1172
+ digestAlgorithm: options.digestAlgorithm,
1173
+ signAssertion: options.signAssertion,
1174
+ cert: options.cert,
1175
+ key: options.key,
1176
+ issuer: options.issuer,
1177
+ lifetimeInSeconds: options.lifetimeInSeconds || 3600,
1178
+ audiences: options.audience,
1179
+ attributes: claims || void 0,
1180
+ nameIdentifier: ni.nameIdentifier,
1181
+ nameIdentifierFormat: ni.nameIdentifierFormat || options.nameIdentifierFormat,
1182
+ nameIdentifierNameQualifier: ni.nameIdentifierNameQualifier,
1183
+ nameIdentifierSPNameQualifier: ni.nameIdentifierSPNameQualifier,
1184
+ nameIdentifierSPProvidedID: ni.nameIdentifierSPProvidedID,
1185
+ recipient: options.recipient,
1186
+ inResponseTo: options.inResponseTo,
1187
+ authnContextClassRef: options.authnContextClassRef,
1188
+ encryptionPublicKey: options.encryptionPublicKey,
1189
+ encryptionCert: options.encryptionCert,
1190
+ encryptionAlgorithm: options.encryptionAlgorithm,
1191
+ keyEncryptionAlgorithm: options.keyEncryptionAlgorithm,
1192
+ sessionIndex: options.sessionIndex,
1193
+ typedAttributes: options.typedAttributes,
1194
+ includeAttributeNameFormat: options.includeAttributeNameFormat,
1195
+ signatureNamespacePrefix: options.signatureNamespacePrefix,
1196
+ transformAssertion: options.transformAssertion
1197
+ });
1198
+ return buildSamlResponse({
1199
+ ...options,
1200
+ samlAssertion,
1201
+ samlStatusCode: options.samlStatusCode || STATUS.SUCCESS
1202
+ });
1203
+ }
1204
+ async function authenticate(input, options, user) {
1205
+ const opts = {
1206
+ ...options,
1207
+ signatureAlgorithm: options.signatureAlgorithm || "rsa-sha256",
1208
+ digestAlgorithm: options.digestAlgorithm || "sha256"
1209
+ };
1210
+ if (typeof opts.getPostURL !== "function") {
1211
+ throw new Error("getPostURL is required");
1212
+ }
1213
+ const samlRequest = input.query.SAMLRequest || input.body.SAMLRequest;
1214
+ const samlRequestDom = await parseSamlRequest(input, samlRequest, "AUTHN_REQUEST", {
1215
+ ...opts,
1216
+ relayState: opts.RelayState || input.query.RelayState
1217
+ });
1218
+ let audience = opts.audience;
1219
+ if (samlRequestDom) {
1220
+ if (!audience) {
1221
+ audience = selectNodes(ISSUER_XPATH, samlRequestDom)[0]?.textContent ?? void 0;
1222
+ }
1223
+ const id = samlRequestDom.documentElement?.getAttribute("ID");
1224
+ if (id) opts.inResponseTo = opts.inResponseTo || id;
1225
+ }
1226
+ const postUrl = await opts.getPostURL(audience, samlRequestDom);
1227
+ if (!postUrl) throw new SamlpError("Unable to determine the Assertion Consumer Service URL", 401);
1228
+ if (!user) throw new SamlpError("Unauthorized", 401);
1229
+ opts.audience = audience;
1230
+ opts.postUrl = postUrl;
1231
+ const xml = await createSamlResponse(opts, user);
1232
+ return {
1233
+ url: postUrl,
1234
+ field: "SAMLResponse",
1235
+ value: Buffer.from(xml).toString("base64"),
1236
+ relayState: opts.RelayState || input.query.RelayState || input.body.RelayState || "",
1237
+ xml,
1238
+ options: opts
1239
+ };
1240
+ }
1241
+ async function parseRequest(input, options = {}) {
1242
+ const samlRequest = input.query.SAMLRequest || input.body.SAMLRequest;
1243
+ if (!samlRequest) return void 0;
1244
+ const dom = await parseSamlRequest(input, samlRequest, "AUTHN_REQUEST", options);
1245
+ if (!dom) return void 0;
1246
+ const data = {};
1247
+ const root = dom.documentElement;
1248
+ const issuer = selectNodes(ISSUER_XPATH, dom);
1249
+ if (issuer.length > 0) data.issuer = issuer[0].textContent ?? void 0;
1250
+ const subject = selectNodes(
1251
+ "//*[local-name(.)='Subject' and namespace-uri(.)='urn:oasis:names:tc:SAML:2.0:assertion']/*[local-name(.)='NameID']",
1252
+ dom
1253
+ );
1254
+ if (subject.length > 0) data.subject = subject[0].textContent ?? void 0;
1255
+ const acs = root.getAttribute("AssertionConsumerServiceURL");
1256
+ if (acs) data.assertionConsumerServiceURL = acs;
1257
+ const destination = root.getAttribute("Destination");
1258
+ if (destination) data.destination = destination;
1259
+ const forceAuthn = root.getAttribute("ForceAuthn");
1260
+ if (forceAuthn) data.forceAuthn = forceAuthn;
1261
+ const id = root.getAttribute("ID");
1262
+ if (id) data.id = id;
1263
+ const classRefs = selectNodes(ELEMENTS.AUTHN_REQUEST.AUTHN_CONTEXT_CLASS_REF_PATH, dom);
1264
+ if (classRefs.length === 1) {
1265
+ data.requestedAuthnContext = { authnContextClassRef: classRefs[0].textContent ?? "" };
1266
+ }
1267
+ return data;
1268
+ }
1269
+ function matchingIndex(issuer, sessionIndex, nameId) {
1270
+ return (session) => {
1271
+ if (session.serviceProviderId && issuer && session.serviceProviderId !== issuer) {
1272
+ return false;
1273
+ }
1274
+ const hasMatchingSessionIndex = Array.isArray(sessionIndex) ? sessionIndex.some((si) => si === session.sessionIndex) : sessionIndex === session.sessionIndex;
1275
+ return hasMatchingSessionIndex && session.nameId === nameId;
1276
+ };
1277
+ }
1278
+ class SessionParticipants {
1279
+ participants;
1280
+ constructor(sessions = []) {
1281
+ this.participants = sessions;
1282
+ }
1283
+ /** Finds a participant by issuer (optional), session index and NameID. */
1284
+ get(issuer, sessionIndices, nameId) {
1285
+ return this.participants.find(matchingIndex(issuer, sessionIndices, nameId));
1286
+ }
1287
+ hasElements() {
1288
+ return this.participants.length > 0;
1289
+ }
1290
+ /** Returns the first participant without removing it. */
1291
+ getFirst() {
1292
+ return this.participants[0];
1293
+ }
1294
+ /** Removes and returns the matching participant (if any). */
1295
+ remove(issuer, sessionIndex, nameId) {
1296
+ if (this.participants.length === 0 || !issuer) return null;
1297
+ const index = this.participants.findIndex(matchingIndex(issuer, sessionIndex, nameId));
1298
+ if (index === -1) return null;
1299
+ return this.participants.splice(index, 1)[0];
1300
+ }
1301
+ }
1302
+ const whitespace = /^\s+$/;
1303
+ function removeEmptyNodes(node) {
1304
+ for (let i = 0; i < node.childNodes.length; i++) {
1305
+ const current = node.childNodes[i];
1306
+ if (current.nodeType === 3 && whitespace.test(current.nodeValue ?? "")) {
1307
+ node.removeChild(current);
1308
+ i--;
1309
+ } else if (current.nodeType === 1) {
1310
+ removeEmptyNodes(current);
1311
+ }
1312
+ }
1313
+ }
1314
+ function trimXml(xml) {
1315
+ const dom = new DOMParser().parseFromString(xml, "text/xml");
1316
+ removeEmptyNodes(dom);
1317
+ return new XMLSerializer().serializeToString(dom);
1318
+ }
1319
+ const deflateRawAsync = promisify(deflateRaw);
1320
+ const inflateRawAsync = promisify(inflateRaw);
1321
+ async function parseIncomingLogoutRequest(input, samlRequest, options) {
1322
+ const type = "LOGOUT_REQUEST";
1323
+ const dom = await parseSamlRequest(input, samlRequest, type, options);
1324
+ const descriptor = ELEMENTS[type];
1325
+ const data = {};
1326
+ const issuer = selectNodes(descriptor.ISSUER_PATH, dom);
1327
+ if (issuer.length > 0) data.issuer = issuer[0].textContent ?? void 0;
1328
+ const sessionIndexNodes = selectNodes("//*[local-name(.)='SessionIndex']/text()", dom);
1329
+ if (sessionIndexNodes.length > 0) data.sessionIndices = sessionIndexNodes.map((n) => n.textContent ?? "");
1330
+ const nameId = selectNodes("//*[local-name(.)='NameID']", dom);
1331
+ if (nameId.length > 0) {
1332
+ data.nameId = nameId[0].textContent ?? void 0;
1333
+ data.nameIdFormat = nameId[0].getAttribute("Format") ?? void 0;
1334
+ }
1335
+ const root = dom.documentElement;
1336
+ const destination = root.getAttribute("Destination");
1337
+ if (destination) data.destination = destination;
1338
+ const id = root.getAttribute("ID");
1339
+ if (id) data.id = id;
1340
+ const signature = selectNodes(descriptor.SIGNATURE_VALIDATION_PATH, dom);
1341
+ if (signature.length > 0) data.signature = signature[0].textContent ?? void 0;
1342
+ return data;
1343
+ }
1344
+ async function prepareAndSendToken(elementType, token, options, ctx) {
1345
+ const type = ELEMENTS[elementType].PROP;
1346
+ const params = { [type]: "", RelayState: ctx.relayState || "" };
1347
+ const send = () => {
1348
+ if (options.protocolBinding !== BINDINGS.HTTP_REDIRECT) {
1349
+ return {
1350
+ type: "post",
1351
+ url: ctx.destination,
1352
+ field: type,
1353
+ value: params[type],
1354
+ relayState: params.RelayState ?? ""
1355
+ };
1356
+ }
1357
+ try {
1358
+ return { type: "redirect", url: appendQueryString(ctx.destination, params) };
1359
+ } catch {
1360
+ throw new Error("The logout URL may be missing or misconfigured");
1361
+ }
1362
+ };
1363
+ const canonical = trimXml(token);
1364
+ const signingOptions = {
1365
+ key: options.key,
1366
+ cert: options.cert,
1367
+ signatureAlgorithm: options.signatureAlgorithm,
1368
+ digestAlgorithm: options.digestAlgorithm,
1369
+ reference: ctx.reference
1370
+ };
1371
+ if (options.protocolBinding !== BINDINGS.HTTP_REDIRECT || !options.deflate) {
1372
+ params[type] = Buffer.from(signXml(signingOptions, canonical)).toString("base64");
1373
+ return send();
1374
+ }
1375
+ params[type] = (await deflateRawAsync(Buffer.from(canonical))).toString("base64");
1376
+ if (params.RelayState === "") delete params.RelayState;
1377
+ params.SigAlg = getSigAlg(options);
1378
+ params.Signature = sign(options, qs.stringify(params));
1379
+ return send();
1380
+ }
1381
+ async function prepareAndSendLogoutRequest(options, transactionId) {
1382
+ const { sessionParticipants } = options;
1383
+ if (!sessionParticipants.hasElements()) {
1384
+ return finalize(options, transactionId);
1385
+ }
1386
+ const participant = sessionParticipants.getFirst();
1387
+ const relayState = options.store.save({
1388
+ transactionId,
1389
+ sessionIndex: participant.sessionIndex,
1390
+ issuer: participant.serviceProviderId,
1391
+ nameId: participant.nameId
1392
+ });
1393
+ const logoutRequest$1 = logoutRequest({
1394
+ ID: generateUniqueID(),
1395
+ IssueInstant: generateInstant(),
1396
+ Issuer: options.issuer,
1397
+ // IdP identifier
1398
+ NameID: { value: participant.nameId, Format: participant.nameIdFormat },
1399
+ SessionIndex: participant.sessionIndex,
1400
+ Destination: participant.serviceProviderLogoutURL
1401
+ });
1402
+ return prepareAndSendToken("LOGOUT_REQUEST", logoutRequest$1, options, {
1403
+ destination: participant.serviceProviderLogoutURL,
1404
+ relayState
1405
+ });
1406
+ }
1407
+ async function finalize(options, transactionId) {
1408
+ const transaction = options.store.load(transactionId, { destroy: true });
1409
+ let isPartialLogout = !!transaction && transaction.global_status === "failed";
1410
+ try {
1411
+ await options.clearIdPSession?.();
1412
+ } catch {
1413
+ isPartialLogout = true;
1414
+ }
1415
+ if (!transaction || !transaction.parsedRequest) {
1416
+ return { type: "done" };
1417
+ }
1418
+ const data = transaction.parsedRequest;
1419
+ const destination = data.serviceProviderLogoutURL || options.destination;
1420
+ const logoutResponse$1 = logoutResponse({
1421
+ id: "_" + generateUniqueID(),
1422
+ instant: generateInstant(),
1423
+ inResponseTo: data.id,
1424
+ destination,
1425
+ issuer: options.issuer,
1426
+ samlStatusCode: isPartialLogout ? STATUS.PARTIAL_LOGOUT : STATUS.SUCCESS,
1427
+ samlStatusMessage: options.samlStatusMessage
1428
+ });
1429
+ return prepareAndSendToken("LOGOUT_RESPONSE", logoutResponse$1, options, {
1430
+ destination,
1431
+ // We stored the relay state of the initial request
1432
+ relayState: transaction.relayState,
1433
+ reference: ELEMENTS.LOGOUT_RESPONSE.SIGNATURE_LOCATION_PATH
1434
+ });
1435
+ }
1436
+ async function validateSamlResponse(input, options, cert) {
1437
+ const samlResponse2 = input.query.SAMLResponse || input.body.SAMLResponse;
1438
+ const raw2 = Buffer.from(samlResponse2, "base64");
1439
+ const xmlString = input.body.SAMLResponse || !options.deflate ? raw2.toString() : (await inflateRawAsync(raw2)).toString();
1440
+ const xml = parseXml(xmlString);
1441
+ const PROTOCOL_NS = "urn:oasis:names:tc:SAML:2.0:protocol";
1442
+ const parsed = {};
1443
+ const statusCodes = xml.getElementsByTagNameNS(PROTOCOL_NS, "StatusCode");
1444
+ if (statusCodes[0]) {
1445
+ parsed.status = statusCodes[0].getAttribute("Value") ?? void 0;
1446
+ if (statusCodes[1]) parsed.subCode = statusCodes[1].getAttribute("Value") ?? void 0;
1447
+ }
1448
+ const message = xml.getElementsByTagNameNS(PROTOCOL_NS, "StatusMessage")[0];
1449
+ if (message) parsed.message = message.textContent ?? void 0;
1450
+ const detail = xml.getElementsByTagNameNS(PROTOCOL_NS, "StatusDetail")[0];
1451
+ if (detail) parsed.detail = detail.textContent ?? void 0;
1452
+ const issuer = xml.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion", "Issuer")[0];
1453
+ if (issuer) parsed.issuer = issuer.textContent ?? void 0;
1454
+ if (cert) {
1455
+ validateSignature(input, "LOGOUT_RESPONSE", xml, xmlString, {
1456
+ signingCert: cert,
1457
+ deflate: options.deflate
1458
+ });
1459
+ }
1460
+ return parsed;
1461
+ }
1462
+ async function logout(input, options) {
1463
+ const relayStateParam = input.query.RelayState || input.body.RelayState;
1464
+ const samlRequest = input.query.SAMLRequest || input.body.SAMLRequest;
1465
+ if (samlRequest) {
1466
+ const requestData = await parseIncomingLogoutRequest(input, samlRequest, {
1467
+ getCredentials: (issuer, sessionIndices, nameId) => {
1468
+ const session2 = options.sessionParticipants.get(issuer, sessionIndices, nameId);
1469
+ if (!session2) throw new Error("Invalid Session Participant");
1470
+ if (!session2.cert) return void 0;
1471
+ return { cert: session2.cert, thumbprints: session2.thumbprint };
1472
+ }
1473
+ });
1474
+ if (!requestData.issuer) {
1475
+ throw new SamlpError("SAML Request with no issuer. Issuer is a mandatory element.", 400);
1476
+ }
1477
+ const session = options.sessionParticipants.get(
1478
+ requestData.issuer,
1479
+ requestData.sessionIndices,
1480
+ requestData.nameId
1481
+ );
1482
+ if (!session && !options.destination) {
1483
+ throw new SamlpError("Invalid Session Participant", 400);
1484
+ }
1485
+ const transactionId2 = options.store.save({
1486
+ parsedRequest: {
1487
+ id: requestData.id,
1488
+ serviceProviderLogoutURL: session?.serviceProviderLogoutURL || options.destination
1489
+ },
1490
+ relayState: relayStateParam
1491
+ });
1492
+ options.sessionParticipants.remove(session?.serviceProviderId, session?.sessionIndex, session?.nameId);
1493
+ return prepareAndSendLogoutRequest(options, transactionId2);
1494
+ }
1495
+ if (input.query.SAMLResponse || input.body.SAMLResponse) {
1496
+ const state = options.store.load(relayStateParam, { destroy: true });
1497
+ if (!state) throw new SamlpError("Invalid RelayState", 400);
1498
+ const participant = options.sessionParticipants.get(state.issuer, [state.sessionIndex], state.nameId);
1499
+ if (!participant) throw new SamlpError("Invalid Session Participant", 400);
1500
+ const logoutResponse2 = await validateSamlResponse(input, options, participant.cert);
1501
+ const transactionId2 = state.transactionId;
1502
+ if (logoutResponse2.status !== STATUS.SUCCESS) {
1503
+ const globalState = options.store.load(transactionId2) ?? {};
1504
+ globalState.global_status = "failed";
1505
+ options.store.update(transactionId2, globalState);
1506
+ }
1507
+ options.sessionParticipants.remove(state.issuer, state.sessionIndex, state.nameId);
1508
+ return prepareAndSendLogoutRequest(options, transactionId2);
1509
+ }
1510
+ const transactionId = options.store.save({});
1511
+ return prepareAndSendLogoutRequest(options, transactionId);
1512
+ }
1513
+ function buildMetadata(options, origin) {
1514
+ if (!options.issuer) throw new Error("options.issuer is required");
1515
+ if (!options.cert) throw new Error("options.cert is required");
1516
+ const pem = removeHeaders(options.cert);
1517
+ if (!pem) throw new Error("options.cert must be a PEM certificate");
1518
+ const logoutPaths = options.logoutEndpointPaths ?? { redirect: "/logout" };
1519
+ const logoutEndpoints = {};
1520
+ if (logoutPaths.redirect) logoutEndpoints.redirect = origin + logoutPaths.redirect;
1521
+ if (logoutPaths.post) logoutEndpoints.post = origin + logoutPaths.post;
1522
+ return metadata({
1523
+ claimTypes: (options.profileMapper ?? passportProfileMapper).metadata,
1524
+ pem,
1525
+ issuer: options.issuer,
1526
+ redirectEndpoint: origin + (options.redirectEndpointPath ?? ""),
1527
+ postEndpoint: origin + (options.postEndpointPath ?? ""),
1528
+ logoutEndpoints
1529
+ }).replace(/\n(?:\s*\n)+/g, "\n");
1530
+ }
1531
+ const AutoPostForm = ({ url, field, value, relayState }) => /* @__PURE__ */ jsxs(Fragment, { children: [
1532
+ raw("<!DOCTYPE html>"),
1533
+ /* @__PURE__ */ jsxs("html", { children: [
1534
+ /* @__PURE__ */ jsx("head", { children: /* @__PURE__ */ jsx("title", { children: "Keep Calm and Single Sign-On!" }) }),
1535
+ /* @__PURE__ */ jsxs("body", { children: [
1536
+ /* @__PURE__ */ jsxs("form", { method: "post", name: "hiddenform", action: url, children: [
1537
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: field, value }),
1538
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "RelayState", value: relayState ?? "" }),
1539
+ /* @__PURE__ */ jsxs("noscript", { children: [
1540
+ /* @__PURE__ */ jsx("p", { children: "JavaScript is disabled. Click Submit to continue." }),
1541
+ /* @__PURE__ */ jsx("input", { type: "submit", value: "Submit" })
1542
+ ] })
1543
+ ] }),
1544
+ /* @__PURE__ */ jsx("script", { children: raw("window.setTimeout(function(){document.forms[0].submit();}, 0);") })
1545
+ ] })
1546
+ ] })
1547
+ ] });
1548
+ const STYLES = `
1549
+ :root { color-scheme: light; }
1550
+ * { box-sizing: border-box; }
1551
+ body { margin: 0; font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; color: #2c3e50; background: #f5f7f9; }
1552
+ nav { background: #2d3f50; color: #ecf0f1; display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; }
1553
+ nav a { color: #ecf0f1; text-decoration: none; margin-left: 20px; }
1554
+ nav a.brand { margin-left: 0; font-size: 16px; font-weight: 600; }
1555
+ main { max-width: 820px; margin: 24px auto; padding: 0 16px; }
1556
+ .panel { background: #fff; border: 1px solid #d9e0e6; border-radius: 4px; margin-bottom: 20px; }
1557
+ .panel > h3, .panel > summary { margin: 0; padding: 10px 16px; font-size: 15px; font-weight: 600; background: #eef2f5; border-bottom: 1px solid #d9e0e6; cursor: default; }
1558
+ .panel.primary > h3, .panel.primary > summary { background: #2d3f50; color: #ecf0f1; }
1559
+ .panel > summary { cursor: pointer; }
1560
+ .panel-body { padding: 16px; }
1561
+ .row { display: flex; align-items: flex-start; margin-bottom: 12px; gap: 12px; }
1562
+ .row > label, .row > .label { flex: 0 0 200px; padding-top: 7px; font-weight: 600; }
1563
+ .row > .field { flex: 1; min-width: 0; }
1564
+ .static { padding-top: 7px; overflow-wrap: anywhere; }
1565
+ input, select { width: 100%; padding: 6px 10px; border: 1px solid #c3ccd4; border-radius: 4px; font: inherit; }
1566
+ .hint { color: #7b8a97; font-size: 12px; margin: 4px 0 0; }
1567
+ .actions { display: flex; justify-content: flex-end; gap: 10px; }
1568
+ .btn { display: inline-block; padding: 7px 16px; border: 1px solid #c3ccd4; border-radius: 4px; background: #fff; color: #2c3e50; font: inherit; text-decoration: none; cursor: pointer; }
1569
+ .btn.primary { background: #2d3f50; border-color: #2d3f50; color: #fff; }
1570
+ pre { white-space: pre-wrap; word-break: break-word; }
1571
+ `;
1572
+ const Layout = ({ issuer, children }) => /* @__PURE__ */ jsxs(Fragment, { children: [
1573
+ raw("<!DOCTYPE html>"),
1574
+ /* @__PURE__ */ jsxs("html", { lang: "en", children: [
1575
+ /* @__PURE__ */ jsxs("head", { children: [
1576
+ /* @__PURE__ */ jsx("meta", { charset: "utf-8" }),
1577
+ /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
1578
+ /* @__PURE__ */ jsx("title", { children: "Simple Identity Provider" }),
1579
+ /* @__PURE__ */ jsx("style", { children: raw(STYLES) })
1580
+ ] }),
1581
+ /* @__PURE__ */ jsxs("body", { children: [
1582
+ /* @__PURE__ */ jsxs("nav", { children: [
1583
+ /* @__PURE__ */ jsxs("a", { class: "brand", href: IDP_PATHS.METADATA, children: [
1584
+ "Simple SAML Identity Provider",
1585
+ issuer ? ` (${issuer})` : ""
1586
+ ] }),
1587
+ /* @__PURE__ */ jsxs("div", { children: [
1588
+ /* @__PURE__ */ jsx("a", { href: IDP_PATHS.METADATA, children: "IdP Metadata" }),
1589
+ /* @__PURE__ */ jsx("a", { href: IDP_PATHS.SIGN_OUT, children: "New Session" }),
1590
+ /* @__PURE__ */ jsx("a", { href: IDP_PATHS.SETTINGS, children: "Settings" })
1591
+ ] })
1592
+ ] }),
1593
+ /* @__PURE__ */ jsx("main", { children })
1594
+ ] })
1595
+ ] })
1596
+ ] });
1597
+ const ErrorPage = ({ message, status, stack, issuer }) => /* @__PURE__ */ jsx(Layout, { issuer, children: /* @__PURE__ */ jsxs("div", { id: "container", children: [
1598
+ /* @__PURE__ */ jsx("h1", { children: message }),
1599
+ status ? /* @__PURE__ */ jsx("h2", { children: status }) : null,
1600
+ stack ? /* @__PURE__ */ jsx("pre", { children: stack }) : null,
1601
+ /* @__PURE__ */ jsx("div", { class: "actions", children: /* @__PURE__ */ jsx("form", { action: "/", method: "get", children: /* @__PURE__ */ jsx("button", { id: "btn-return", class: "btn primary", type: "submit", children: "Restart" }) }) })
1602
+ ] }) });
1603
+ const AUTHN_CONTEXT_CLASSES = [
1604
+ "InternetProtocol",
1605
+ "InternetProtocolPassword",
1606
+ "Kerberos",
1607
+ "MobileOneFactorUnregistered",
1608
+ "MobileTwoFactorUnregistered",
1609
+ "MobileOneFactorContract",
1610
+ "MobileTwoFactorContract",
1611
+ "Password",
1612
+ "PasswordProtectedTransport",
1613
+ "PreviousSession",
1614
+ "X509",
1615
+ "PGP",
1616
+ "SPKI",
1617
+ "XMLDSig",
1618
+ "Smartcard",
1619
+ "SmartcardPKI",
1620
+ "SoftwarePKI",
1621
+ "Telephony",
1622
+ "NomadTelephony",
1623
+ "PersonalTelephony",
1624
+ "AuthenticatedTelephony",
1625
+ "SecureRemotePassword",
1626
+ "TLSClient",
1627
+ "TimeSyncToken",
1628
+ "unspecified"
1629
+ ].map((name2) => ({ value: `urn:oasis:names:tc:SAML:2.0:ac:classes:${name2}`, label: name2 }));
1630
+ const Row = ({ id, label, children }) => /* @__PURE__ */ jsxs("div", { class: "row", children: [
1631
+ /* @__PURE__ */ jsx("label", { for: id, children: label }),
1632
+ /* @__PURE__ */ jsx("div", { class: "field", children })
1633
+ ] });
1634
+ const Select = ({ id, name: name2, selected, options }) => /* @__PURE__ */ jsx("select", { id, name: name2 ?? id, children: options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, selected: o.value === String(selected), children: o.label })) });
1635
+ const YES_NO = [
1636
+ { value: "true", label: "Yes" },
1637
+ { value: "false", label: "No" }
1638
+ ];
1639
+ const SettingsPage = ({ idp }) => /* @__PURE__ */ jsx(Layout, { issuer: idp.issuer, children: /* @__PURE__ */ jsxs("form", { id: "settings", method: "post", children: [
1640
+ /* @__PURE__ */ jsxs("div", { class: "panel", children: [
1641
+ /* @__PURE__ */ jsx("h3", { children: "Identity Provider Settings" }),
1642
+ /* @__PURE__ */ jsxs("div", { class: "panel-body", children: [
1643
+ /* @__PURE__ */ jsx(Row, { id: "issuer", label: "Issuer", children: /* @__PURE__ */ jsx("input", { id: "issuer", name: "issuer", type: "text", required: true, value: idp.issuer }) }),
1644
+ /* @__PURE__ */ jsx(Row, { id: "signatureAlgorithm", label: "Signature Algorithm", children: /* @__PURE__ */ jsx(
1645
+ Select,
1646
+ {
1647
+ id: "signatureAlgorithm",
1648
+ selected: idp.signatureAlgorithm,
1649
+ options: [
1650
+ { value: "rsa-sha1", label: "RSA-SHA1" },
1651
+ { value: "rsa-sha256", label: "RSA-SHA256" }
1652
+ ]
1653
+ }
1654
+ ) }),
1655
+ /* @__PURE__ */ jsx(Row, { id: "digestAlgorithm", label: "Digest Algorithm", children: /* @__PURE__ */ jsx(
1656
+ Select,
1657
+ {
1658
+ id: "digestAlgorithm",
1659
+ selected: idp.digestAlgorithm,
1660
+ options: [
1661
+ { value: "sha1", label: "SHA1" },
1662
+ { value: "sha256", label: "SHA256" }
1663
+ ]
1664
+ }
1665
+ ) }),
1666
+ /* @__PURE__ */ jsx(Row, { id: "signResponse", label: "Sign Response", children: /* @__PURE__ */ jsx(Select, { id: "signResponse", selected: idp.signResponse, options: YES_NO }) }),
1667
+ /* @__PURE__ */ jsx(Row, { id: "lifetimeInSeconds", label: "Assertion Lifetime Seconds", children: /* @__PURE__ */ jsx("input", { id: "lifetimeInSeconds", name: "lifetimeInSeconds", type: "number", required: true, value: idp.lifetimeInSeconds }) }),
1668
+ /* @__PURE__ */ jsx(Row, { id: "authnContextClassRef", label: "Authentication Context Class", children: /* @__PURE__ */ jsx(
1669
+ Select,
1670
+ {
1671
+ id: "authnContextClassRef",
1672
+ selected: idp.authnContextClassRef,
1673
+ options: AUTHN_CONTEXT_CLASSES
1674
+ }
1675
+ ) }),
1676
+ /* @__PURE__ */ jsx(Row, { id: "allowRequestAcsUrl", label: "Allow Dynamic Assertion Consumer Service URL", children: /* @__PURE__ */ jsx(Select, { id: "allowRequestAcsUrl", selected: idp.allowRequestAcsUrl, options: YES_NO }) })
1677
+ ] })
1678
+ ] }),
1679
+ /* @__PURE__ */ jsxs("div", { class: "panel", children: [
1680
+ /* @__PURE__ */ jsx("h3", { children: "Service Provider Settings" }),
1681
+ /* @__PURE__ */ jsxs("div", { class: "panel-body", children: [
1682
+ /* @__PURE__ */ jsx(Row, { id: "serviceProviderId", label: "Issuer URI", children: /* @__PURE__ */ jsx("input", { id: "serviceProviderId", name: "serviceProviderId", type: "text", required: true, value: idp.serviceProviderId }) }),
1683
+ /* @__PURE__ */ jsx(Row, { id: "sloUrl", label: "Single Logout URL", children: /* @__PURE__ */ jsx("input", { id: "sloUrl", name: "sloUrl", type: "url", value: idp.sloUrl ?? "" }) }),
1684
+ /* @__PURE__ */ jsx(Row, { id: "acsUrl", label: "Assertion Consumer Service URL", children: /* @__PURE__ */ jsx("input", { id: "acsUrl", name: "acsUrl", type: "url", required: true, value: idp.acsUrl }) }),
1685
+ /* @__PURE__ */ jsx(Row, { id: "destination", label: "Destination", children: /* @__PURE__ */ jsx("input", { id: "destination", name: "destination", type: "url", required: true, value: idp.destination }) }),
1686
+ /* @__PURE__ */ jsx(Row, { id: "recipient", label: "Recipient", children: /* @__PURE__ */ jsx("input", { id: "recipient", name: "recipient", type: "url", required: true, value: idp.recipient }) }),
1687
+ /* @__PURE__ */ jsx(Row, { id: "audience", label: "Audience", children: /* @__PURE__ */ jsx("input", { id: "audience", name: "audience", type: "text", required: true, value: idp.audience }) }),
1688
+ /* @__PURE__ */ jsx(Row, { id: "relayState", label: "Default Relay State", children: /* @__PURE__ */ jsx("input", { id: "relayState", name: "RelayState", type: "text", value: idp.RelayState ?? "" }) })
1689
+ ] })
1690
+ ] }),
1691
+ /* @__PURE__ */ jsxs("div", { class: "actions", children: [
1692
+ /* @__PURE__ */ jsx("a", { href: "/", class: "btn", children: "Cancel" }),
1693
+ /* @__PURE__ */ jsx("button", { type: "submit", class: "btn primary", children: "Save" })
1694
+ ] })
1695
+ ] }) });
1696
+ const NAME_ID_FORMATS = [
1697
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
1698
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
1699
+ "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
1700
+ "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
1701
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName",
1702
+ "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName",
1703
+ "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos"
1704
+ ];
1705
+ const StaticRow = ({ id, label, value }) => /* @__PURE__ */ jsxs("div", { class: "row", children: [
1706
+ /* @__PURE__ */ jsx("span", { class: "label", children: label }),
1707
+ /* @__PURE__ */ jsx("div", { class: "field", children: /* @__PURE__ */ jsx("p", { class: "static", id, children: String(value) }) })
1708
+ ] });
1709
+ const AuthnRequestPanel = ({ authnRequest }) => /* @__PURE__ */ jsxs("details", { class: "panel primary", id: "authn-request", children: [
1710
+ /* @__PURE__ */ jsx("summary", { children: "SAML Authentication Request" }),
1711
+ /* @__PURE__ */ jsxs("div", { class: "panel-body", children: [
1712
+ /* @__PURE__ */ jsx(StaticRow, { id: "requestId", label: "Request ID", value: authnRequest.id }),
1713
+ /* @__PURE__ */ jsx(StaticRow, { id: "issuer", label: "Issuer", value: authnRequest.issuer }),
1714
+ authnRequest.destination ? /* @__PURE__ */ jsx(StaticRow, { id: "destination", label: "Destination", value: authnRequest.destination }) : null,
1715
+ authnRequest.acsUrl ? /* @__PURE__ */ jsx(StaticRow, { id: "acsUrl", label: "ACS URL", value: authnRequest.acsUrl }) : null,
1716
+ authnRequest.forceAuthn ? /* @__PURE__ */ jsx(StaticRow, { id: "forceAuthn", label: "ForceAuthn", value: authnRequest.forceAuthn }) : null,
1717
+ authnRequest.relayState ? /* @__PURE__ */ jsx(StaticRow, { id: "relayState", label: "Relay State", value: authnRequest.relayState }) : null
1718
+ ] })
1719
+ ] });
1720
+ const AttributeField = ({ entry, value }) => /* @__PURE__ */ jsxs("div", { class: "row", children: [
1721
+ /* @__PURE__ */ jsx("label", { for: entry.id, children: entry.displayName }),
1722
+ /* @__PURE__ */ jsxs("div", { class: "field", children: [
1723
+ entry.options ? /* @__PURE__ */ jsx("select", { id: entry.id, name: entry.id, children: entry.options.map((option) => /* @__PURE__ */ jsx("option", { value: option, selected: option === value, children: option })) }) : /* @__PURE__ */ jsx("input", { id: entry.id, name: entry.id, type: "text", placeholder: "Attribute value", value: value ?? "" }),
1724
+ entry.multiValue ? /* @__PURE__ */ jsx("p", { class: "hint", children: "Multiple values are separated by commas" }) : null
1725
+ ] })
1726
+ ] });
1727
+ const UserPage = ({ user, sessionIndex, metadata: metadata2, authnRequest, idp }) => /* @__PURE__ */ jsxs(Layout, { issuer: idp.issuer, children: [
1728
+ authnRequest?.id ? /* @__PURE__ */ jsx(AuthnRequestPanel, { authnRequest }) : null,
1729
+ /* @__PURE__ */ jsxs("form", { action: IDP_PATHS.SIGN_IN, method: "post", children: [
1730
+ /* @__PURE__ */ jsxs("div", { class: "panel primary", children: [
1731
+ /* @__PURE__ */ jsx("h3", { children: "SAML Assertion" }),
1732
+ /* @__PURE__ */ jsxs("div", { class: "panel-body", children: [
1733
+ /* @__PURE__ */ jsxs("div", { class: "row", children: [
1734
+ /* @__PURE__ */ jsx("label", { for: "userName", children: "Subject NameID" }),
1735
+ /* @__PURE__ */ jsx("div", { class: "field", children: /* @__PURE__ */ jsx("input", { id: "userName", name: "userName", placeholder: "user@example.com", required: true, value: user.userName }) })
1736
+ ] }),
1737
+ /* @__PURE__ */ jsxs("details", { children: [
1738
+ /* @__PURE__ */ jsx("summary", { children: "More Options" }),
1739
+ /* @__PURE__ */ jsxs("div", { class: "row", children: [
1740
+ /* @__PURE__ */ jsx("label", { for: "nameIdFormat", children: "Name ID Format" }),
1741
+ /* @__PURE__ */ jsx("div", { class: "field", children: /* @__PURE__ */ jsx("select", { id: "nameIdFormat", name: "nameIdFormat", children: NAME_ID_FORMATS.map((format) => /* @__PURE__ */ jsx("option", { value: format, selected: format === user.nameIdFormat, children: format })) }) })
1742
+ ] }),
1743
+ /* @__PURE__ */ jsxs("div", { class: "row", children: [
1744
+ /* @__PURE__ */ jsx("label", { for: "nameIdNameQualifier", children: "Name Qualifier" }),
1745
+ /* @__PURE__ */ jsx("div", { class: "field", children: /* @__PURE__ */ jsx("input", { id: "nameIdNameQualifier", name: "nameIdNameQualifier", type: "text", value: user.nameIdNameQualifier ?? "" }) })
1746
+ ] }),
1747
+ /* @__PURE__ */ jsxs("div", { class: "row", children: [
1748
+ /* @__PURE__ */ jsx("label", { for: "nameIdSPNameQualifier", children: "Service Provider Name Qualifier" }),
1749
+ /* @__PURE__ */ jsx("div", { class: "field", children: /* @__PURE__ */ jsx("input", { id: "nameIdSPNameQualifier", name: "nameIdSPNameQualifier", type: "text", value: user.nameIdSPNameQualifier ?? "" }) })
1750
+ ] }),
1751
+ /* @__PURE__ */ jsxs("div", { class: "row", children: [
1752
+ /* @__PURE__ */ jsx("label", { for: "nameIdSPProvidedID", children: "Service Provider Provided ID" }),
1753
+ /* @__PURE__ */ jsx("div", { class: "field", children: /* @__PURE__ */ jsx("input", { id: "nameIdSPProvidedID", name: "nameIdSPProvidedID", type: "text", value: user.nameIdSPProvidedID ?? "" }) })
1754
+ ] })
1755
+ ] })
1756
+ ] })
1757
+ ] }),
1758
+ /* @__PURE__ */ jsxs("details", { class: "panel", children: [
1759
+ /* @__PURE__ */ jsx("summary", { children: "Authentication Statement" }),
1760
+ /* @__PURE__ */ jsxs("div", { class: "panel-body", children: [
1761
+ /* @__PURE__ */ jsx(StaticRow, { id: "sessionIndex", label: "Session Index", value: sessionIndex }),
1762
+ /* @__PURE__ */ jsx(StaticRow, { id: "authnContextClassRef", label: "Authentication Context Class", value: idp.authnContextClassRef })
1763
+ ] })
1764
+ ] }),
1765
+ /* @__PURE__ */ jsxs("div", { class: "panel", children: [
1766
+ /* @__PURE__ */ jsx("h3", { children: "Attribute Statements" }),
1767
+ /* @__PURE__ */ jsx("div", { class: "panel-body", children: metadata2.map((entry) => /* @__PURE__ */ jsx(AttributeField, { entry, value: user[entry.id] })) })
1768
+ ] }),
1769
+ authnRequest?.id ? /* @__PURE__ */ jsx(
1770
+ "input",
1771
+ {
1772
+ name: "_authnRequest",
1773
+ type: "hidden",
1774
+ value: Buffer.from(JSON.stringify(authnRequest)).toString("base64")
1775
+ }
1776
+ ) : null,
1777
+ /* @__PURE__ */ jsxs("div", { class: "actions", children: [
1778
+ /* @__PURE__ */ jsx("a", { id: "btn-sign-out", class: "btn", href: IDP_PATHS.SIGN_OUT, children: "Sign out" }),
1779
+ /* @__PURE__ */ jsx("button", { id: "btn-sign-in", class: "btn primary", type: "submit", children: "Sign in" })
1780
+ ] })
1781
+ ] })
1782
+ ] });
1783
+ const SSO_PATHS = ["/", "/idp", IDP_PATHS.SSO];
1784
+ const UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1785
+ async function readInput(c) {
1786
+ const query = c.req.query();
1787
+ const body = {};
1788
+ if (c.req.method === "POST") {
1789
+ const contentType = c.req.header("content-type") ?? "";
1790
+ if (/application\/x-www-form-urlencoded|multipart\/form-data/.test(contentType)) {
1791
+ const parsed = await c.req.parseBody();
1792
+ for (const [key, value] of Object.entries(parsed)) {
1793
+ if (typeof value === "string") body[key] = value;
1794
+ }
1795
+ }
1796
+ }
1797
+ return { query, body };
1798
+ }
1799
+ function errorPage(c, state, message, status = 500, error) {
1800
+ const stack = error instanceof Error ? error.stack : void 0;
1801
+ return c.html(
1802
+ /* @__PURE__ */ jsx(ErrorPage, { message, status, stack, issuer: state.settings.issuer }),
1803
+ status
1804
+ );
1805
+ }
1806
+ function postFormResponse(c, form) {
1807
+ return c.html(/* @__PURE__ */ jsx(AutoPostForm, { url: form.url, field: form.field, value: form.value, relayState: form.relayState }));
1808
+ }
1809
+ function toBoolean(value) {
1810
+ return ["true", "yes", "1", "on"].includes(value.toLowerCase());
1811
+ }
1812
+ const SETTINGS_FIELDS = {
1813
+ issuer: "string",
1814
+ signatureAlgorithm: "string",
1815
+ digestAlgorithm: "string",
1816
+ signResponse: "boolean",
1817
+ lifetimeInSeconds: "number",
1818
+ authnContextClassRef: "string",
1819
+ allowRequestAcsUrl: "boolean",
1820
+ serviceProviderId: "string",
1821
+ sloUrl: "optional",
1822
+ acsUrl: "string",
1823
+ destination: "string",
1824
+ recipient: "string",
1825
+ audience: "string",
1826
+ RelayState: "optional"
1827
+ };
1828
+ function applySettings(settings, body) {
1829
+ for (const [key, kind] of Object.entries(SETTINGS_FIELDS)) {
1830
+ const value = body[key];
1831
+ if (value === void 0) continue;
1832
+ const target = settings;
1833
+ switch (kind) {
1834
+ case "boolean":
1835
+ target[key] = toBoolean(value);
1836
+ break;
1837
+ case "number": {
1838
+ const parsed = Number.parseInt(value, 10);
1839
+ if (!Number.isNaN(parsed)) target[key] = parsed;
1840
+ break;
1841
+ }
1842
+ case "optional":
1843
+ target[key] = value === "" ? void 0 : value;
1844
+ break;
1845
+ default:
1846
+ target[key] = value;
1847
+ }
1848
+ }
1849
+ }
1850
+ function authnContextDeclTransformer(decl, logger2) {
1851
+ if (!decl) return void 0;
1852
+ return (assertionDom) => {
1853
+ let declDoc;
1854
+ try {
1855
+ declDoc = new DOMParser().parseFromString(decl, "text/xml");
1856
+ } catch (err) {
1857
+ logger2.log("Unable to parse Authentication Context Declaration XML", err);
1858
+ return;
1859
+ }
1860
+ if (!declDoc?.documentElement) return;
1861
+ const declEl = assertionDom.createElementNS("urn:oasis:names:tc:SAML:2.0:assertion", "saml:AuthnContextDecl");
1862
+ declEl.appendChild(assertionDom.importNode(declDoc.documentElement, true));
1863
+ assertionDom.getElementsByTagName("saml:AuthnContext")[0].appendChild(declEl);
1864
+ };
1865
+ }
1866
+ function requestOrigin(c) {
1867
+ const url = new URL(c.req.url);
1868
+ const proto = c.req.header("x-forwarded-proto")?.split(",")[0].trim() ?? url.protocol.replace(":", "");
1869
+ const host = c.req.header("x-forwarded-host") ?? c.req.header("host") ?? url.host;
1870
+ return `${proto}://${host}`;
1871
+ }
1872
+ function createApp({ state, logger: logger$1 = createLogger("info"), rollSession = false }) {
1873
+ const app = new Hono();
1874
+ const fail = (c, st, message, status = 500, error) => {
1875
+ if (status >= 500) logger$1.error(error instanceof Error ? error.stack ?? message : message);
1876
+ return errorPage(c, st, message, status, error);
1877
+ };
1878
+ const sessions = new SessionStore();
1879
+ const profileMapper = createSimpleProfileMapper(() => state.metadata);
1880
+ const logoutCredentials = (c) => {
1881
+ const settings = state.settings;
1882
+ const session = c.get("session");
1883
+ return {
1884
+ participant: {
1885
+ serviceProviderId: settings.serviceProviderId,
1886
+ sessionIndex: getSessionIndex(session),
1887
+ nameId: state.user.userName,
1888
+ nameIdFormat: state.user.nameIdFormat,
1889
+ serviceProviderLogoutURL: settings.sloUrl
1890
+ },
1891
+ session
1892
+ };
1893
+ };
1894
+ app.use(
1895
+ "*",
1896
+ logger((message, ...rest) => logger$1.log(message, ...rest))
1897
+ );
1898
+ app.use("*", sessionMiddleware(sessions, randomBytes(32).toString("hex")));
1899
+ app.use("*", async (c, next) => {
1900
+ if (rollSession && SSO_PATHS.includes(c.req.path) && ["GET", "POST"].includes(c.req.method)) {
1901
+ c.get("session").regenerate();
1902
+ }
1903
+ await next();
1904
+ });
1905
+ const showUser = (c, authnRequest) => {
1906
+ const session = c.get("session");
1907
+ return c.html(
1908
+ /* @__PURE__ */ jsx(
1909
+ UserPage,
1910
+ {
1911
+ user: state.user,
1912
+ sessionIndex: getSessionIndex(session),
1913
+ metadata: state.metadata,
1914
+ authnRequest,
1915
+ idp: state.settings
1916
+ }
1917
+ )
1918
+ );
1919
+ };
1920
+ const parseSamlRequest2 = async (c) => {
1921
+ const input = await readInput(c);
1922
+ let data;
1923
+ try {
1924
+ data = await parseRequest(input);
1925
+ } catch (err) {
1926
+ return fail(c, state, "SAML AuthnRequest Parse Error: " + err.message, 400, err);
1927
+ }
1928
+ let authnRequest;
1929
+ if (data) {
1930
+ authnRequest = {
1931
+ relayState: input.query.RelayState || input.body.RelayState,
1932
+ id: data.id,
1933
+ issuer: data.issuer,
1934
+ destination: data.destination,
1935
+ acsUrl: data.assertionConsumerServiceURL,
1936
+ forceAuthn: data.forceAuthn === "true"
1937
+ };
1938
+ logger$1.log("Received AuthnRequest => \n", authnRequest);
1939
+ }
1940
+ return showUser(c, authnRequest);
1941
+ };
1942
+ app.on(["GET", "POST"], SSO_PATHS, parseSamlRequest2);
1943
+ const handleLogout = async (c) => {
1944
+ const settings = state.settings;
1945
+ if (!settings.sloUrl) {
1946
+ return fail(c, state, "SAML Single Logout Service URL not defined for Service Provider", 400);
1947
+ }
1948
+ const { participant, session } = logoutCredentials(c);
1949
+ logger$1.log("Processing SAML SLO request for participant => \n", participant);
1950
+ const input = await readInput(c);
1951
+ let result;
1952
+ try {
1953
+ result = await logout(input, {
1954
+ issuer: settings.issuer,
1955
+ cert: settings.cert,
1956
+ key: settings.key,
1957
+ digestAlgorithm: settings.digestAlgorithm,
1958
+ signatureAlgorithm: settings.signatureAlgorithm,
1959
+ sessionParticipants: new SessionParticipants([participant]),
1960
+ store: createLogoutStore(session),
1961
+ clearIdPSession: () => {
1962
+ logger$1.log("Destroying session " + session.id + " for participant", participant);
1963
+ session.destroy();
1964
+ }
1965
+ });
1966
+ } catch (err) {
1967
+ const status = err instanceof SamlpError ? err.status : 500;
1968
+ return fail(c, state, err.message, status, err);
1969
+ }
1970
+ switch (result.type) {
1971
+ case "post":
1972
+ return postFormResponse(c, result);
1973
+ case "redirect":
1974
+ return c.redirect(result.url);
1975
+ default:
1976
+ return c.text("OK", 200);
1977
+ }
1978
+ };
1979
+ app.get(IDP_PATHS.SLO, handleLogout);
1980
+ app.post(IDP_PATHS.SLO, handleLogout);
1981
+ app.post(IDP_PATHS.SIGN_IN, async (c) => {
1982
+ const settings = state.settings;
1983
+ const session = c.get("session");
1984
+ const input = await readInput(c);
1985
+ const authOptions = {
1986
+ ...settings,
1987
+ profileMapper,
1988
+ postEndpointPath: IDP_PATHS.SSO,
1989
+ redirectEndpointPath: IDP_PATHS.SSO,
1990
+ getPostURL: () => settings.acsUrl,
1991
+ transformAssertion: authnContextDeclTransformer(settings.authnContextDecl, logger$1)
1992
+ };
1993
+ let authnRequest;
1994
+ for (const [key, value] of Object.entries(input.body)) {
1995
+ if (value === void 0) continue;
1996
+ if (key === "_authnRequest") {
1997
+ try {
1998
+ authnRequest = JSON.parse(Buffer.from(value, "base64").toString("utf8"));
1999
+ } catch {
2000
+ return fail(c, state, "Invalid _authnRequest", 400);
2001
+ }
2002
+ if (!authnRequest) continue;
2003
+ authOptions.inResponseTo = authnRequest.id;
2004
+ if (settings.allowRequestAcsUrl && authnRequest.acsUrl) {
2005
+ authOptions.acsUrl = authnRequest.acsUrl;
2006
+ authOptions.recipient = authnRequest.acsUrl;
2007
+ authOptions.destination = authnRequest.acsUrl;
2008
+ authOptions.forceAuthn = authnRequest.forceAuthn;
2009
+ authOptions.getPostURL = () => authnRequest.acsUrl;
2010
+ }
2011
+ if (authnRequest.relayState) {
2012
+ authOptions.RelayState = authnRequest.relayState;
2013
+ }
2014
+ } else if (!UNSAFE_KEYS.has(key)) {
2015
+ state.user[key] = value;
2016
+ }
2017
+ }
2018
+ if (!authOptions.encryptAssertion) {
2019
+ delete authOptions.encryptionCert;
2020
+ delete authOptions.encryptionPublicKey;
2021
+ }
2022
+ authOptions.sessionIndex = getSessionIndex(session);
2023
+ logger$1.log(
2024
+ [
2025
+ "Generating SAML Response using =>",
2026
+ " User =>",
2027
+ describeRecord(state.user, 4, (_k, v) => String(v)),
2028
+ " SAMLP Options =>",
2029
+ describeRecord(authOptions, 4),
2030
+ ""
2031
+ ].join("\n")
2032
+ );
2033
+ try {
2034
+ const result = await authenticate({ query: {}, body: {} }, authOptions, state.user);
2035
+ logger$1.log(
2036
+ [
2037
+ `Sending SAML Response to ${result.url} =>`,
2038
+ ` RelayState =>`,
2039
+ ` ${result.relayState || "None"}`,
2040
+ ` SAMLResponse =>`,
2041
+ prettyPrintXml(result.xml, 4),
2042
+ ""
2043
+ ].join("\n")
2044
+ );
2045
+ return postFormResponse(c, result);
2046
+ } catch (err) {
2047
+ const status = err instanceof SamlpError ? err.status : 500;
2048
+ return fail(c, state, err.message, status, err);
2049
+ }
2050
+ });
2051
+ app.get(IDP_PATHS.METADATA, (c) => {
2052
+ const settings = state.settings;
2053
+ const xml = buildMetadata(
2054
+ {
2055
+ issuer: settings.issuer,
2056
+ cert: settings.cert,
2057
+ profileMapper,
2058
+ postEndpointPath: IDP_PATHS.SSO,
2059
+ redirectEndpointPath: IDP_PATHS.SSO,
2060
+ logoutEndpointPaths: settings.sloUrl ? { redirect: IDP_PATHS.SLO, post: IDP_PATHS.SLO } : {}
2061
+ },
2062
+ requestOrigin(c)
2063
+ );
2064
+ return c.body(xml, 200, { "Content-Type": "application/xml" });
2065
+ });
2066
+ app.post(IDP_PATHS.METADATA, async (c) => {
2067
+ const { body } = await readInput(c);
2068
+ if (body.attributeName && body.displayName) {
2069
+ const attribute = {
2070
+ id: body.attributeName,
2071
+ optional: true,
2072
+ displayName: body.displayName,
2073
+ description: body.description || "",
2074
+ multiValue: body.valueType === "multi"
2075
+ };
2076
+ const index = state.metadata.findIndex((entry) => entry.id === attribute.id);
2077
+ if (index === -1) state.metadata.push(attribute);
2078
+ else state.metadata[index] = attribute;
2079
+ logger$1.log("Updated SAML Attribute Metadata => \n", state.metadata);
2080
+ return c.body(null, 200);
2081
+ }
2082
+ return c.body(null, 400);
2083
+ });
2084
+ app.get(IDP_PATHS.SIGN_OUT, (c) => {
2085
+ const settings = state.settings;
2086
+ if (settings.sloUrl) {
2087
+ logger$1.log(
2088
+ `Initiating SAML SLO request for user: ${state.user.userName} with sessionIndex: ${getSessionIndex(c.get("session"))}`
2089
+ );
2090
+ return c.redirect(IDP_PATHS.SLO);
2091
+ }
2092
+ logger$1.log("SAML SLO is not enabled for SP, destroying IDP session");
2093
+ c.get("session").destroy();
2094
+ return c.redirect(c.req.header("referer") || "/");
2095
+ });
2096
+ app.get(IDP_PATHS.SETTINGS, (c) => c.html(/* @__PURE__ */ jsx(SettingsPage, { idp: state.settings })));
2097
+ app.post(IDP_PATHS.SETTINGS, async (c) => {
2098
+ const { body } = await readInput(c);
2099
+ applySettings(state.settings, body);
2100
+ logger$1.log("Updated IdP Configuration => \n", {
2101
+ ...state.settings,
2102
+ cert: formatOptionValue("cert", state.settings.cert),
2103
+ key: formatOptionValue("key", state.settings.key)
2104
+ });
2105
+ return c.redirect("/");
2106
+ });
2107
+ app.notFound((c) => {
2108
+ const err = new Error("Route Not Found");
2109
+ return fail(c, state, err.message, 404, err);
2110
+ });
2111
+ app.onError((err, c) => {
2112
+ const status = err.status ?? 500;
2113
+ return fail(c, state, err.message, status, err);
2114
+ });
2115
+ return app;
2116
+ }
2117
+ const WILDCARD_ADDRESSES = ["0.0.0.0", "::"];
2118
+ async function createIdp(input) {
2119
+ const options = await resolveOptions(input);
2120
+ const logger2 = createLogger(options.logLevel);
2121
+ const state = createState(options);
2122
+ const app = createApp({ state, logger: logger2, rollSession: options.rollSession });
2123
+ return { app, state, options, logger: logger2 };
2124
+ }
2125
+ async function runServer(input) {
2126
+ const idp = await createIdp(input);
2127
+ const { app, options, logger: logger2 } = idp;
2128
+ logger2.log(configBanner(options));
2129
+ logger2.log(`Starting IdP server on port ${pc.cyan(`${options.host}:${options.port}`)}...
2130
+ `);
2131
+ const server = await new Promise((resolve2, reject) => {
2132
+ const instance = serve(
2133
+ {
2134
+ fetch: app.fetch,
2135
+ port: options.port,
2136
+ hostname: options.host,
2137
+ ...options.https ? {
2138
+ createServer,
2139
+ serverOptions: { key: options.httpsPrivateKey, cert: options.httpsCert }
2140
+ } : {}
2141
+ },
2142
+ () => resolve2(instance)
2143
+ );
2144
+ instance.once("error", reject);
2145
+ });
2146
+ const { address, port } = server.address();
2147
+ const scheme = options.https ? "https" : "http";
2148
+ const bannerHost = WILDCARD_ADDRESSES.includes(address) ? hostname() : "localhost";
2149
+ const url = `${scheme}://localhost:${port}`;
2150
+ logger2.log(listeningBanner(`${scheme}://${bannerHost}:${port}`, !!options.sloUrl));
2151
+ return {
2152
+ ...idp,
2153
+ url,
2154
+ metadataUrl: `${url}/metadata`,
2155
+ server,
2156
+ close: () => new Promise((resolve2, reject) => {
2157
+ server.close((err) => err ? reject(err) : resolve2());
2158
+ server.closeAllConnections();
2159
+ })
2160
+ };
2161
+ }
2162
+ export {
2163
+ IDP_PATHS,
2164
+ LOG_LEVELS as L,
2165
+ OptionsError,
2166
+ createDefaultConfig,
2167
+ createIdp,
2168
+ runServer
2169
+ };
2170
+ //# sourceMappingURL=index.js.map