@voltro/plugin-sso-saml 0.32.0 → 0.34.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.
package/dist/index.js CHANGED
@@ -1,26 +1,26 @@
1
1
  import { Effect as e, Schema as t } from "effect";
2
2
  import { FetchHttpClient as n, HttpClient as r } from "@effect/platform";
3
3
  import { randomBytes as i } from "node:crypto";
4
- import { definePlugin as a } from "@voltro/protocol";
5
- import { buildSetCookie as o, readCookie as s, signSession as c } from "@voltro/protocol/session";
6
- import { id as l, table as u, text as d } from "@voltro/database";
4
+ import { definePlugin as a, pluginInstanceName as o } from "@voltro/protocol";
5
+ import { buildSetCookie as s, readCookie as c, signSession as l } from "@voltro/protocol/session";
6
+ import { id as u, table as d, text as f } from "@voltro/database";
7
7
  //#region src/saml.ts
8
- var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/g, ""), p = (e) => {
8
+ var p = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/g, ""), m = (e) => {
9
9
  let t = e.signingCert ? `
10
10
  <KeyDescriptor use="signing">
11
11
  <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
12
- <X509Data><X509Certificate>${m(f(e.signingCert))}</X509Certificate></X509Data>
12
+ <X509Data><X509Certificate>${h(p(e.signingCert))}</X509Certificate></X509Data>
13
13
  </KeyInfo>
14
14
  </KeyDescriptor>` : "", n = e.sloUrl ? `
15
- <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="${m(e.sloUrl)}"/>` : "";
15
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="${h(e.sloUrl)}"/>` : "";
16
16
  return `<?xml version="1.0"?>
17
- <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${m(e.entityId)}">
17
+ <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${h(e.entityId)}">
18
18
  <SPSSODescriptor AuthnRequestsSigned="${e.authnRequestsSigned ? "true" : "false"}" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">${t}${n}
19
19
  <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress</NameIDFormat>
20
- <AssertionConsumerService index="0" isDefault="true" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${m(e.acsUrl)}"/>
20
+ <AssertionConsumerService index="0" isDefault="true" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${h(e.acsUrl)}"/>
21
21
  </SPSSODescriptor>
22
22
  </EntityDescriptor>`;
23
- }, m = (e) => e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"), h = (e) => {
23
+ }, h = (e) => e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"), g = (e) => {
24
24
  let t = e.attributes ?? {}, n = (...n) => {
25
25
  for (let r of n) {
26
26
  let n = e[r] ?? t[r];
@@ -35,20 +35,20 @@ var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/
35
35
  displayName: n("displayName", "name", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "cn"),
36
36
  attributes: t
37
37
  };
38
- }, g = "_voltro_saml_replay", _ = u(g, {
39
- id: l({ prefix: "samlrq" }),
40
- requestId: d().unique(),
41
- instant: d(),
42
- createdAtMs: d()
43
- }).index("bySamlRequestId", ["requestId"]), v = [_], y = (e, t) => ({
38
+ }, _ = "_voltro_saml_replay", v = d(_, {
39
+ id: u({ prefix: "samlrq" }),
40
+ requestId: f().unique(),
41
+ instant: f(),
42
+ createdAtMs: f()
43
+ }).index("bySamlRequestId", ["requestId"]), y = [v], b = (e, t) => ({
44
44
  kind: "binary",
45
45
  op: "eq",
46
46
  column: e,
47
47
  value: t
48
- }), b = (e, t = 10 * 6e4) => {
48
+ }), x = (e, t = 10 * 6e4) => {
49
49
  let n = async (t) => (await e.query({
50
- table: g,
51
- predicate: y("requestId", t),
50
+ table: _,
51
+ predicate: b("requestId", t),
52
52
  order: [{
53
53
  column: "id",
54
54
  direction: "asc"
@@ -64,7 +64,7 @@ var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/
64
64
  saveAsync: async (t, n) => {
65
65
  let r = Date.now();
66
66
  try {
67
- return await e.insert(g, {
67
+ return await e.insert(_, {
68
68
  requestId: t,
69
69
  instant: n,
70
70
  createdAtMs: String(r)
@@ -78,15 +78,15 @@ var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/
78
78
  },
79
79
  getAsync: async (t) => {
80
80
  let i = await n(t);
81
- return i ? r(i, Date.now()) ? (await e.delete(g, String(i.id)).catch(() => void 0), null) : String(i.instant) : null;
81
+ return i ? r(i, Date.now()) ? (await e.delete(_, String(i.id)).catch(() => void 0), null) : String(i.instant) : null;
82
82
  },
83
83
  removeAsync: async (t) => {
84
84
  if (t == null) return null;
85
85
  let r = await n(t);
86
- return r ? (await e.delete(g, String(r.id)).catch(() => void 0), t) : null;
86
+ return r ? (await e.delete(_, String(r.id)).catch(() => void 0), t) : null;
87
87
  }
88
88
  };
89
- }, x = (e = 10 * 6e4) => {
89
+ }, S = (e = 10 * 6e4) => {
90
90
  let t = /* @__PURE__ */ new Map();
91
91
  return {
92
92
  saveAsync: async (e, n) => {
@@ -103,26 +103,26 @@ var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/
103
103
  },
104
104
  removeAsync: async (e) => e == null || !t.has(e) ? null : (t.delete(e), e)
105
105
  };
106
- }, S = "_voltro_saml_logout", C = u(S, {
107
- id: l({ prefix: "samllo" }),
108
- sloKey: d().unique(),
109
- nameId: d(),
110
- nameIdFormat: d(),
111
- sessionIndex: d(),
112
- createdAtMs: d()
113
- }).index("bySamlSloKey", ["sloKey"]), w = [C], T = (e, t) => ({
106
+ }, C = "_voltro_saml_logout", w = d(C, {
107
+ id: u({ prefix: "samllo" }),
108
+ sloKey: f().unique(),
109
+ nameId: f(),
110
+ nameIdFormat: f(),
111
+ sessionIndex: f(),
112
+ createdAtMs: f()
113
+ }).index("bySamlSloKey", ["sloKey"]), T = [w], E = (e, t) => ({
114
114
  kind: "binary",
115
115
  op: "eq",
116
116
  column: e,
117
117
  value: t
118
- }), E = (e) => ({
118
+ }), D = (e) => ({
119
119
  nameId: String(e.nameId ?? ""),
120
120
  ...e.nameIdFormat ? { nameIdFormat: String(e.nameIdFormat) } : {},
121
121
  ...e.sessionIndex ? { sessionIndex: String(e.sessionIndex) } : {}
122
- }), D = (e, t = 10080 * 6e4) => {
122
+ }), O = (e, t = 10080 * 6e4) => {
123
123
  let n = async (t) => (await e.query({
124
- table: S,
125
- predicate: T("sloKey", t),
124
+ table: C,
125
+ predicate: E("sloKey", t),
126
126
  order: [{
127
127
  column: "id",
128
128
  direction: "asc"
@@ -134,12 +134,12 @@ var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/
134
134
  let r = Number(e.createdAtMs);
135
135
  return Number.isFinite(r) && n - r > t;
136
136
  }, i = async (t) => {
137
- await e.delete(S, String(t.id)).catch(() => void 0);
137
+ await e.delete(C, String(t.id)).catch(() => void 0);
138
138
  };
139
139
  return {
140
140
  save: async (t) => {
141
141
  let r = await n(t.sloKey);
142
- r && await i(r), await e.insert(S, {
142
+ r && await i(r), await e.insert(C, {
143
143
  sloKey: t.sloKey,
144
144
  nameId: t.nameId,
145
145
  nameIdFormat: t.nameIdFormat ?? "",
@@ -149,14 +149,14 @@ var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/
149
149
  },
150
150
  load: async (e) => {
151
151
  let t = await n(e);
152
- return t ? r(t, Date.now()) ? (await i(t), null) : E(t) : null;
152
+ return t ? r(t, Date.now()) ? (await i(t), null) : D(t) : null;
153
153
  },
154
154
  remove: async (e) => {
155
155
  let t = await n(e);
156
156
  t && await i(t);
157
157
  }
158
158
  };
159
- }, O = (e = 10080 * 6e4) => {
159
+ }, k = (e = 10080 * 6e4) => {
160
160
  let t = /* @__PURE__ */ new Map();
161
161
  return {
162
162
  save: async (e) => {
@@ -177,112 +177,116 @@ var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/
177
177
  t.delete(e);
178
178
  }
179
179
  };
180
- }, k = class extends t.TaggedError()("SamlMetadataFetchError", {
180
+ }, A = class extends t.TaggedError()("SamlMetadataFetchError", {
181
181
  url: t.String,
182
182
  reason: t.String
183
- }) {}, A = class extends t.TaggedError()("SamlMetadataParseError", {
183
+ }) {}, j = class extends t.TaggedError()("SamlMetadataParseError", {
184
184
  url: t.String,
185
185
  reason: t.String
186
- }) {}, j = "urn:oasis:names:tc:SAML:2.0:metadata", M = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", N = (e) => e.slice(e.indexOf(":") + 1), P = (e, t) => (RegExp(`\\b${t}\\s*=\\s*"([^"]*)"`).exec(e) ?? RegExp(`\\b${t}\\s*=\\s*'([^']*)'`).exec(e))?.[1], F = (e, t) => {
186
+ }) {}, M = "urn:oasis:names:tc:SAML:2.0:metadata", N = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", P = (e) => e.slice(e.indexOf(":") + 1), F = (e, t) => (RegExp(`\\b${t}\\s*=\\s*"([^"]*)"`).exec(e) ?? RegExp(`\\b${t}\\s*=\\s*'([^']*)'`).exec(e))?.[1], I = (e, t) => {
187
187
  let n = [], r = /<([A-Za-z][\w.:-]*)\b([^>]*)>/g, i;
188
- for (; (i = r.exec(e)) !== null;) N(i[1]) === t && n.push(i[0]);
188
+ for (; (i = r.exec(e)) !== null;) P(i[1]) === t && n.push(i[0]);
189
189
  return n;
190
- }, I = (e) => {
190
+ }, L = (e) => {
191
191
  let t = [], n = /<([A-Za-z][\w.:-]*KeyDescriptor)\b([^>]*)>/g, r;
192
192
  for (; (r = n.exec(e)) !== null;) {
193
- if (N(r[1]) !== "KeyDescriptor") continue;
193
+ if (P(r[1]) !== "KeyDescriptor") continue;
194
194
  let n = RegExp(`</${r[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}>`), i = e.slice(r.index + r[0].length), a = n.exec(i), o = a ? i.slice(0, a.index) : i;
195
195
  t.push({
196
- use: P(r[0], "use"),
196
+ use: F(r[0], "use"),
197
197
  body: o
198
198
  });
199
199
  }
200
200
  return t;
201
- }, L = (e) => {
202
- let t = I(e), n = t.find((e) => e.use === "signing") ?? t.find((e) => e.use === void 0) ?? t[0];
201
+ }, R = (e) => {
202
+ let t = L(e), n = t.find((e) => e.use === "signing") ?? t.find((e) => e.use === void 0) ?? t[0];
203
203
  if (!n) return;
204
204
  let r = /<[^>]*X509Certificate[^>]*>([\s\S]*?)<\/[^>]*X509Certificate>/.exec(n.body)?.[1]?.replace(/\s+/g, "");
205
205
  return r && r.length > 0 ? r : void 0;
206
- }, R = (t, n) => e.gen(function* () {
207
- if (!t.includes(j) && !/EntityDescriptor/.test(t)) return yield* e.fail(new A({
206
+ }, z = (t, n) => e.gen(function* () {
207
+ if (!t.includes(M) && !/EntityDescriptor/.test(t)) return yield* e.fail(new j({
208
208
  url: n,
209
209
  reason: "not-saml-metadata"
210
210
  }));
211
- let r = F(t, "SingleSignOnService"), i = r.find((e) => P(e, "Binding") === M) ?? r[0], a = i ? P(i, "Location") : void 0;
212
- if (!a) return yield* e.fail(new A({
211
+ let r = I(t, "SingleSignOnService"), i = r.find((e) => F(e, "Binding") === N) ?? r[0], a = i ? F(i, "Location") : void 0;
212
+ if (!a) return yield* e.fail(new j({
213
213
  url: n,
214
214
  reason: "no-entryPoint"
215
215
  }));
216
- let o = L(t);
217
- if (!o) return yield* e.fail(new A({
216
+ let o = R(t);
217
+ if (!o) return yield* e.fail(new j({
218
218
  url: n,
219
219
  reason: "no-signing-cert"
220
220
  }));
221
- let s = F(t, "SingleLogoutService"), c = s.find((e) => P(e, "Binding") === M) ?? s[0], l = c ? P(c, "Location") : void 0, u = F(t, "EntityDescriptor")[0], d = u ? P(u, "entityID") : void 0;
221
+ let s = I(t, "SingleLogoutService"), c = s.find((e) => F(e, "Binding") === N) ?? s[0], l = c ? F(c, "Location") : void 0, u = I(t, "EntityDescriptor")[0], d = u ? F(u, "entityID") : void 0;
222
222
  return {
223
223
  entryPoint: a,
224
224
  idpCert: o,
225
225
  ...l ? { logoutUrl: l } : {},
226
226
  ...d ? { issuer: d } : {}
227
227
  };
228
- }), z = (t) => e.gen(function* () {
229
- let n = yield* (yield* r.HttpClient).get(t).pipe(e.mapError(() => new k({
228
+ }), B = (t) => e.gen(function* () {
229
+ let n = yield* (yield* r.HttpClient).get(t).pipe(e.mapError(() => new A({
230
230
  url: t,
231
231
  reason: "network"
232
232
  })));
233
- if (n.status < 200 || n.status >= 300) return yield* e.fail(new k({
233
+ if (n.status < 200 || n.status >= 300) return yield* e.fail(new A({
234
234
  url: t,
235
235
  reason: `status ${n.status}`
236
236
  }));
237
- let i = yield* n.text.pipe(e.mapError(() => new k({
237
+ let i = yield* n.text.pipe(e.mapError(() => new A({
238
238
  url: t,
239
239
  reason: "body"
240
240
  })));
241
- return i.trim() ? yield* R(i, t) : yield* e.fail(new k({
241
+ return i.trim() ? yield* z(i, t) : yield* e.fail(new A({
242
242
  url: t,
243
243
  reason: "empty-body"
244
244
  }));
245
- }), B = (e, t) => typeof e != "string" || e[0] !== "/" || /[\u0000-]/.test(e) ? t : e === "/" ? e : e[1] === "/" || e[1] === "\\" ? t : e, V = (e, t, n = "text/plain") => ({
245
+ }), V = (e, t) => typeof e != "string" || e[0] !== "/" || /[\u0000-]/.test(e) ? t : e === "/" ? e : e[1] === "/" || e[1] === "\\" ? t : e, H = (e, t, n = "text/plain") => ({
246
246
  status: e,
247
247
  body: t,
248
248
  contentType: n
249
- }), H = (e, t) => {
249
+ }), U = (e, t) => {
250
250
  let n = { location: e };
251
251
  return typeof t == "string" ? n["set-cookie"] = t : Array.isArray(t) && t.length > 0 && (n["set-cookie"] = t.join(", ")), {
252
252
  status: 302,
253
253
  headers: n
254
254
  };
255
- }, U = ":slo", W = (t) => {
255
+ }, W = ":slo", G = "@voltro/plugin-sso-saml", K = (t) => {
256
256
  if (!t.sessionSecret?.trim()) throw Error("@voltro/plugin-sso-saml: `sessionSecret` must be a non-empty string");
257
257
  if (!t.idp.metadataUrl && (!t.idp.entryPoint || !t.idp.idpCert)) throw Error("@voltro/plugin-sso-saml: provide idp.entryPoint + idp.idpCert, or idp.metadataUrl to load them from the IdP metadata");
258
- let r = t.basePath ?? "/saml", l = t.cookieName ?? "voltro:session", u = `${l}${U}`, d = t.sessionTtlSeconds ?? 3600 * 24 * 7, f = t.successRedirect ?? "/", m = t.logoutRedirect ?? "/", g = t.name ? `@voltro/plugin-sso-saml#${t.name}` : "@voltro/plugin-sso-saml", _ = t.idp.metadataRefreshMs ?? 720 * 6e4, y = t.replayProtection, S = typeof y == "object" && y.store === !0, C = typeof y == "object" ? y.ttlMs ?? 10 * 6e4 : 10 * 6e4, T = typeof t.sloStore == "object" && t.sloStore.store === !0, E = S || T, k, A, j, M, N = 0, P, F = () => {
259
- if (y) {
260
- if (S) {
261
- if (!k) throw Error("@voltro/plugin-sso-saml: replayProtection { store: true } but the DataStore is not bound yet");
262
- return b(k, C);
258
+ let r = t.basePath ?? "/saml", u = t.cookieName ?? "voltro:session", d = `${u}${W}`, f = t.sessionTtlSeconds ?? 3600 * 24 * 7, p = t.successRedirect ?? "/", h = t.logoutRedirect ?? "/", _ = o({
259
+ base: G,
260
+ alias: t.alias,
261
+ instance: t.name
262
+ }), v = t.idp.metadataRefreshMs ?? 720 * 6e4, b = t.replayProtection ?? { store: !0 }, C = typeof b == "object" && b.store === !0, w = typeof b == "object" ? b.ttlMs ?? 10 * 6e4 : 10 * 6e4, E = typeof t.sloStore == "object" && t.sloStore.store === !0, D = C || E, A, j, M, N, P, F = 0, I, L = () => {
263
+ if (b) {
264
+ if (C) {
265
+ if (!A) throw Error("@voltro/plugin-sso-saml: replayProtection { store: true } but the DataStore is not bound yet");
266
+ return x(A, w);
263
267
  }
264
- return x(C);
268
+ return S(w);
265
269
  }
266
- }, I = () => {
267
- if (P) return P;
268
- if (T) {
269
- if (!k) throw Error("@voltro/plugin-sso-saml: sloStore { store: true } but the DataStore is not bound yet");
270
- P = D(k, d * 1e3);
271
- } else P = O(d * 1e3);
272
- return P;
273
- }, L = async () => {
270
+ }, R = () => {
271
+ if (I) return I;
272
+ if (E) {
273
+ if (!A) throw Error("@voltro/plugin-sso-saml: sloStore { store: true } but the DataStore is not bound yet");
274
+ I = O(A, f * 1e3);
275
+ } else I = k(f * 1e3);
276
+ return I;
277
+ }, z = async () => {
274
278
  let r = t.idp.metadataUrl;
275
279
  if (r) {
276
- let i = _ > 0 && Date.now() - N > _;
277
- if (!M || i) {
278
- let t = await e.runPromiseExit(z(r).pipe(e.provide(n.layer)));
279
- t._tag === "Success" && (M = t.value, N = Date.now());
280
+ let i = v > 0 && Date.now() - F > v;
281
+ if (!P || i) {
282
+ let t = await e.runPromiseExit(B(r).pipe(e.provide(n.layer)));
283
+ t._tag === "Success" && (P = t.value, F = Date.now());
280
284
  }
281
- if (M) return {
282
- entryPoint: M.entryPoint,
283
- idpCert: M.idpCert,
284
- ...t.idp.logoutUrl ?? M.logoutUrl ? { logoutUrl: t.idp.logoutUrl ?? M.logoutUrl } : {},
285
- ...t.idp.issuer ?? M.issuer ? { idpIssuer: t.idp.issuer ?? M.issuer } : {}
285
+ if (P) return {
286
+ entryPoint: P.entryPoint,
287
+ idpCert: P.idpCert,
288
+ ...t.idp.logoutUrl ?? P.logoutUrl ? { logoutUrl: t.idp.logoutUrl ?? P.logoutUrl } : {},
289
+ ...t.idp.issuer ?? P.issuer ? { idpIssuer: t.idp.issuer ?? P.issuer } : {}
286
290
  };
287
291
  }
288
292
  return {
@@ -291,7 +295,7 @@ var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/
291
295
  ...t.idp.logoutUrl ? { logoutUrl: t.idp.logoutUrl } : {},
292
296
  ...t.idp.issuer ? { idpIssuer: t.idp.issuer } : {}
293
297
  };
294
- }, R = (e, n) => ({
298
+ }, K = (e, n) => ({
295
299
  callbackUrl: t.sp.acsUrl,
296
300
  entryPoint: e.entryPoint,
297
301
  issuer: t.sp.entityId,
@@ -299,6 +303,7 @@ var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/
299
303
  ...e.idpIssuer ? { idpIssuer: e.idpIssuer } : {},
300
304
  ...e.logoutUrl ? { logoutUrl: e.logoutUrl } : {},
301
305
  wantAssertionsSigned: !0,
306
+ wantAuthnResponseSigned: t.wantAuthnResponseSigned ?? !1,
302
307
  ...t.decryptionPvk ? { decryptionPvk: t.decryptionPvk } : {},
303
308
  ...t.privateKey ? { privateKey: t.privateKey } : {},
304
309
  ...t.signingCert ? { publicCert: t.signingCert } : {},
@@ -306,138 +311,153 @@ var f = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/
306
311
  ...n ? {
307
312
  validateInResponseTo: "always",
308
313
  cacheProvider: n,
309
- requestIdExpirationPeriodMs: C
314
+ requestIdExpirationPeriodMs: w
310
315
  } : {}
311
- }), W = async () => {
312
- let e = !!t.idp.metadataUrl && _ > 0 && Date.now() - N > _;
313
- if (A && !e) return A;
314
- if (!j) try {
315
- let e = await import("@node-saml/node-saml"), t = await L(), n = F();
316
- return A = new e.SAML(R(t, n)), A;
316
+ }), q = async () => {
317
+ let e = !!t.idp.metadataUrl && v > 0 && Date.now() - F > v;
318
+ if (j && !e) return j;
319
+ if (M) return;
320
+ N = void 0;
321
+ let n;
322
+ try {
323
+ n = await import("@node-saml/node-saml");
317
324
  } catch {
318
- j = "SAML SSO requires the optional dependency @node-saml/node-saml — run `pnpm add @node-saml/node-saml`";
325
+ M = "SAML SSO requires the optional dependency @node-saml/node-saml — run `pnpm add @node-saml/node-saml`";
326
+ return;
327
+ }
328
+ try {
329
+ let e = await z(), t = L();
330
+ return j = new n.SAML(K(e, t)), j;
331
+ } catch (e) {
332
+ N = `@voltro/plugin-sso-saml: could not construct the SAML instance — ${String(e?.message ?? e)}`;
319
333
  return;
320
334
  }
321
- }, G = (() => {
335
+ }, J = (() => {
322
336
  try {
323
337
  return new URL(`${r}/slo`, t.sp.acsUrl).toString();
324
338
  } catch {
325
339
  return;
326
340
  }
327
- })(), K = {
341
+ })(), Y = {
328
342
  ...t.sp,
329
343
  authnRequestsSigned: !!t.privateKey,
330
344
  ...t.signingCert ? { signingCert: t.signingCert } : {},
331
- ...G ? { sloUrl: G } : {}
332
- }, q = (e) => s(e.headers.cookie ?? e.headers.Cookie, u), J = () => [o(l, "", {
345
+ ...J ? { sloUrl: J } : {}
346
+ }, X = (e) => c(e.headers.cookie ?? e.headers.Cookie, d), Z = () => [s(u, "", {
333
347
  maxAgeSeconds: 0,
334
348
  httpOnly: !0,
335
349
  sameSite: "lax"
336
- }), o(u, "", {
350
+ }), s(d, "", {
337
351
  maxAgeSeconds: 0,
338
352
  httpOnly: !0,
339
353
  sameSite: "lax"
340
354
  })];
341
355
  return a({
342
- name: g,
356
+ name: _,
357
+ baseName: G,
343
358
  description: "SAML 2.0 SSO — SP-initiated login + Single Logout, ACS, metadata.",
344
- permissions: E ? ["store:write"] : [],
359
+ permissions: D ? ["store:write"] : [],
345
360
  httpRoutes: [{
346
361
  method: "*",
347
362
  path: r,
363
+ originGuard: "exempt",
348
364
  handle: async (e) => {
349
365
  let n = e.path.slice(r.length).replace(/\/+$/, ""), a = e.method.toUpperCase();
350
- if (n === "/metadata" && a === "GET") return V(200, p(K), "application/xml");
351
- let s = await W();
352
- if (!s) return V(500, j ?? "SAML not available");
366
+ if (n === "/metadata" && a === "GET") return H(200, m(Y), "application/xml");
367
+ let o = await q();
368
+ if (!o) return H(500, M ?? N ?? "SAML not available");
353
369
  if (n === "/login" && a === "GET") {
354
- let t = B(new URLSearchParams(e.query).get("returnTo"), f);
355
- return H(await s.getAuthorizeUrlAsync(t, void 0, {}));
370
+ let t = V(new URLSearchParams(e.query).get("returnTo"), p);
371
+ return U(await o.getAuthorizeUrlAsync(t, void 0, {}));
356
372
  }
357
373
  if (n === "/acs" && a === "POST") {
358
- let n = new URLSearchParams(new TextDecoder().decode(e.rawBody)), r = n.get("SAMLResponse"), a = B(n.get("RelayState"), f);
359
- if (!r) return V(400, "missing SAMLResponse");
360
- let p;
374
+ let n = new URLSearchParams(new TextDecoder().decode(e.rawBody)), r = n.get("SAMLResponse"), a = V(n.get("RelayState"), p);
375
+ if (!r) return H(400, "missing SAMLResponse");
376
+ let c;
361
377
  try {
362
- ({profile: p} = await s.validatePostResponseAsync({ SAMLResponse: r }));
378
+ ({profile: c} = await o.validatePostResponseAsync({ SAMLResponse: r }));
363
379
  } catch (e) {
364
- return V(401, `SAML assertion rejected: ${String(e?.message ?? e)}`);
380
+ return H(401, `SAML assertion rejected: ${String(e?.message ?? e)}`);
365
381
  }
366
- if (!p) return V(401, "no SAML profile");
367
- let m = await t.onLogin(h(p));
368
- if (!m) return V(403, "login rejected");
369
- let g = c(m, t.sessionSecret, { ttlSeconds: d }), _ = o(l, g, {
370
- maxAgeSeconds: d,
382
+ if (!c) return H(401, "no SAML profile");
383
+ let m = await t.onLogin(g(c));
384
+ if (!m) return H(403, "login rejected");
385
+ let h = l(m, t.sessionSecret, { ttlSeconds: f }), _ = s(u, h, {
386
+ maxAgeSeconds: f,
371
387
  httpOnly: !0,
372
388
  sameSite: "lax"
373
- }), v = String(p.nameID ?? p.nameId ?? ""), y = [_];
389
+ }), v = String(c.nameID ?? c.nameId ?? ""), y = [_];
374
390
  if (v) {
375
391
  let e = i(24).toString("base64url");
376
- await I().save({
392
+ await R().save({
377
393
  sloKey: e,
378
394
  nameId: v,
379
- ...p.nameIDFormat ? { nameIdFormat: String(p.nameIDFormat) } : {},
380
- ...p.sessionIndex ? { sessionIndex: String(p.sessionIndex) } : {}
381
- }), y.push(o(u, e, {
382
- maxAgeSeconds: d,
395
+ ...c.nameIDFormat ? { nameIdFormat: String(c.nameIDFormat) } : {},
396
+ ...c.sessionIndex ? { sessionIndex: String(c.sessionIndex) } : {}
397
+ }), y.push(s(d, e, {
398
+ maxAgeSeconds: f,
383
399
  httpOnly: !0,
384
400
  sameSite: "lax"
385
401
  }));
386
402
  }
387
- return H(a, y);
403
+ return U(a, y);
388
404
  }
389
405
  if (n === "/logout" && a === "GET") {
390
- let t = B(new URLSearchParams(e.query).get("returnTo"), m), n = q(e), r = n ? await I().load(n) : null;
391
- if (!r) return H(t, J());
406
+ let t = V(new URLSearchParams(e.query).get("returnTo"), h), n = X(e), r = n ? await R().load(n) : null;
407
+ if (!r) return U(t, Z());
392
408
  let i = {
393
409
  nameID: r.nameId,
394
410
  ...r.nameIdFormat ? { nameIDFormat: r.nameIdFormat } : {},
395
411
  ...r.sessionIndex ? { sessionIndex: r.sessionIndex } : {}
396
412
  }, a;
397
413
  try {
398
- a = await s.getLogoutUrlAsync(i, t, {});
414
+ a = await o.getLogoutUrlAsync(i, t, {});
399
415
  } catch (e) {
400
- return V(500, `SAML logout request failed: ${String(e?.message ?? e)}`);
416
+ return H(500, `SAML logout request failed: ${String(e?.message ?? e)}`);
401
417
  }
402
- return n && await I().remove(n), H(a, J());
418
+ return n && await R().remove(n), U(a, Z());
403
419
  }
404
420
  if (n === "/slo" && (a === "GET" || a === "POST")) {
405
421
  let t = a === "GET" ? e.query : new TextDecoder().decode(e.rawBody), n = new URLSearchParams(t), r = {};
406
422
  for (let [e, t] of n) r[e] = t;
407
- let i = B(n.get("RelayState"), m), o;
423
+ let i = V(n.get("RelayState"), h);
424
+ if (!n.has("Signature")) return H(401, "SAML logout rejected: the logout message is not signed");
425
+ let s;
408
426
  try {
409
- o = await s.validateRedirectAsync(r, t);
427
+ s = await o.validateRedirectAsync(r, t);
410
428
  } catch (e) {
411
- return V(401, `SAML logout rejected: ${String(e?.message ?? e)}`);
429
+ return H(401, `SAML logout rejected: ${String(e?.message ?? e)}`);
412
430
  }
413
- if (n.has("SAMLResponse")) return H(i, J());
414
- let c = q(e);
415
- c && await I().remove(c);
431
+ if (n.has("SAMLResponse")) return U(i, Z());
432
+ let c = X(e);
433
+ c && await R().remove(c);
416
434
  try {
417
- return H(await s.getLogoutResponseUrlAsync(o.profile ?? {}, i, {}, !0), J());
435
+ return U(await o.getLogoutResponseUrlAsync(s.profile ?? {}, i, {}, !0), Z());
418
436
  } catch {
419
- return H(i, J());
437
+ return U(i, Z());
420
438
  }
421
439
  }
422
- return V(404, "unknown SAML endpoint");
440
+ return H(404, "unknown SAML endpoint");
423
441
  }
424
442
  }],
425
- ...E ? { extendSchema: { tables: [...S ? v : [], ...T ? w : []] } } : {},
443
+ ...D ? { extendSchema: { tables: [...C ? y : [], ...E ? T : []] } } : {},
426
444
  bindDataStore: (e) => {
427
- k = e;
445
+ A = e;
428
446
  },
429
447
  onActivate: (n) => e.sync(() => {
430
448
  n.logger.info("saml sso active", {
431
449
  basePath: r,
432
450
  entityId: t.sp.entityId,
433
451
  idpSource: t.idp.metadataUrl ? "metadata-url" : "static",
434
- replayProtection: y ? S ? "store" : "memory" : "off",
435
- sloStore: T ? "store" : "memory",
452
+ replayProtection: b ? C ? "store" : "memory" : "off",
453
+ sloStore: E ? "store" : "memory",
436
454
  requestSigning: t.privateKey ? "on" : "off",
437
- encryptedAssertions: t.decryptionPvk ? "on" : "off"
438
- }), y && !S && n.logger.warn("saml sso: replayProtection uses an IN-PROCESS cache — correct for ONE replica only. Under >1 replica a login and its ACS can land on different processes (InResponseTo fails) and a cross-replica replay is not caught. Use replayProtection: { store: true } in production."), T || n.logger.warn("saml sso: SLO state uses an IN-PROCESS store — correct for ONE replica only. Under >1 replica a login and its logout can land on different processes. Use sloStore: { store: true } in production.");
455
+ encryptedAssertions: t.decryptionPvk ? "on" : "off",
456
+ wantAssertionsSigned: "required",
457
+ wantAuthnResponseSigned: t.wantAuthnResponseSigned ?? !1 ? "required" : "optional"
458
+ }), b && !C && n.logger.warn("saml sso: replayProtection uses an IN-PROCESS cache — correct for ONE replica only. Under >1 replica a login and its ACS can land on different processes (InResponseTo fails) and a cross-replica replay is not caught. Use replayProtection: { store: true } in production."), b || n.logger.warn("saml sso: replayProtection is OFF — a captured SAMLResponse can be replayed for as long as its assertion is valid. This also ENABLES IdP-initiated SSO (the app-tile flow), which is the only reason to turn it off. If you did not mean to accept unsolicited responses, remove `replayProtection: false`."), E || n.logger.warn("saml sso: SLO state uses an IN-PROCESS store — correct for ONE replica only. Under >1 replica a login and its logout can land on different processes. Use sloStore: { store: true } in production.");
439
459
  })
440
460
  });
441
461
  };
442
462
  //#endregion
443
- export { S as LOGOUT_SESSION_TABLE, g as REPLAY_CACHE_TABLE, k as SamlMetadataFetchError, A as SamlMetadataParseError, D as dataStoreLogoutStore, b as dataStoreReplayCache, h as extractProfile, z as fetchIdpMetadata, O as memoryLogoutStore, x as memoryReplayCache, R as parseIdpMetadata, C as samlLogoutTable, w as samlLogoutTables, _ as samlReplayTable, v as samlReplayTables, W as samlSsoPlugin, p as spMetadataXml };
463
+ export { C as LOGOUT_SESSION_TABLE, _ as REPLAY_CACHE_TABLE, A as SamlMetadataFetchError, j as SamlMetadataParseError, O as dataStoreLogoutStore, x as dataStoreReplayCache, g as extractProfile, B as fetchIdpMetadata, k as memoryLogoutStore, S as memoryReplayCache, z as parseIdpMetadata, w as samlLogoutTable, T as samlLogoutTables, v as samlReplayTable, y as samlReplayTables, K as samlSsoPlugin, m as spMetadataXml };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-sso-saml",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "Enterprise SAML 2.0 SSO — SP-initiated login, ACS assertion consumer, and SP metadata. Signature verification via @node-saml/node-saml (optional, lazy); the framework integration (routes, session minting, attribute mapping) is built in.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -22,7 +22,8 @@
22
22
  "types": "./dist/index.d.ts",
23
23
  "import": "./dist/index.js",
24
24
  "default": "./dist/index.js"
25
- }
25
+ },
26
+ "./package.json": "./package.json"
26
27
  },
27
28
  "main": "./dist/index.js",
28
29
  "module": "./dist/index.js",
@@ -33,8 +34,8 @@
33
34
  },
34
35
  "dependencies": {
35
36
  "@effect/platform": "^0.97.0",
36
- "@voltro/database": "0.32.0",
37
- "@voltro/protocol": "0.32.0"
37
+ "@voltro/database": "0.34.0",
38
+ "@voltro/protocol": "0.34.0"
38
39
  },
39
40
  "optionalDependencies": {
40
41
  "@node-saml/node-saml": "^5.0.0"