@voltro/plugin-sso-saml 0.84.0 → 0.86.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.
@@ -0,0 +1,73 @@
1
+ import { id as e, table as t, text as n } from "@voltro/database";
2
+ //#region src/saml-cache.ts
3
+ var r = "_voltro_saml_replay", i = t(r, {
4
+ id: e({ prefix: "samlrq" }),
5
+ requestId: n().unique(),
6
+ instant: n(),
7
+ createdAtMs: n()
8
+ }).index("bySamlRequestId", ["requestId"]), a = [i], o = (e, t) => ({
9
+ kind: "binary",
10
+ op: "eq",
11
+ column: e,
12
+ value: t
13
+ }), s = (e, t = 6e5) => {
14
+ let n = async (t) => (await e.query({
15
+ table: r,
16
+ predicate: o("requestId", t),
17
+ order: [{
18
+ column: "id",
19
+ direction: "asc"
20
+ }],
21
+ take: 1,
22
+ skip: void 0,
23
+ projection: void 0
24
+ }))[0], i = (e, n) => {
25
+ let r = Number(e.createdAtMs);
26
+ return Number.isFinite(r) && n - r > t;
27
+ };
28
+ return {
29
+ saveAsync: async (t, n) => {
30
+ let i = Date.now();
31
+ try {
32
+ return await e.insert(r, {
33
+ requestId: t,
34
+ instant: n,
35
+ createdAtMs: String(i)
36
+ }), {
37
+ value: n,
38
+ createdAt: i
39
+ };
40
+ } catch {
41
+ return null;
42
+ }
43
+ },
44
+ getAsync: async (t) => {
45
+ let a = await n(t);
46
+ return a ? i(a, Date.now()) ? (await e.delete(r, String(a.id)).catch(() => void 0), null) : String(a.instant) : null;
47
+ },
48
+ removeAsync: async (t) => {
49
+ if (t == null) return null;
50
+ let i = await n(t);
51
+ return i ? (await e.delete(r, String(i.id)).catch(() => void 0), t) : null;
52
+ }
53
+ };
54
+ }, c = (e = 6e5) => {
55
+ let t = /* @__PURE__ */ new Map();
56
+ return {
57
+ saveAsync: async (e, n) => {
58
+ if (t.has(e)) return null;
59
+ let r = {
60
+ value: n,
61
+ createdAt: Date.now()
62
+ };
63
+ return t.set(e, r), r;
64
+ },
65
+ getAsync: async (n) => {
66
+ let r = t.get(n);
67
+ return r ? Date.now() - r.createdAt > e ? (t.delete(n), null) : r.value : null;
68
+ },
69
+ removeAsync: async (e) => e == null || !t.has(e) ? null : (t.delete(e), e)
70
+ };
71
+ };
72
+ //#endregion
73
+ export { r as REPLAY_CACHE_TABLE, s as dataStoreReplayCache, c as memoryReplayCache, i as samlReplayTable, a as samlReplayTables };
@@ -0,0 +1,11 @@
1
+ import { Schema as e } from "effect";
2
+ //#region src/saml-errors.ts
3
+ var t = class extends e.TaggedError()("SamlMetadataFetchError", {
4
+ url: e.String,
5
+ reason: e.String
6
+ }) {}, n = class extends e.TaggedError()("SamlMetadataParseError", {
7
+ url: e.String,
8
+ reason: e.String
9
+ }) {};
10
+ //#endregion
11
+ export { t as SamlMetadataFetchError, n as SamlMetadataParseError };
@@ -0,0 +1,79 @@
1
+ import { id as e, table as t, text as n } from "@voltro/database";
2
+ //#region src/saml-logout.ts
3
+ var r = "_voltro_saml_logout", i = t(r, {
4
+ id: e({ prefix: "samllo" }),
5
+ sloKey: n().unique(),
6
+ nameId: n(),
7
+ nameIdFormat: n(),
8
+ sessionIndex: n(),
9
+ createdAtMs: n()
10
+ }).index("bySamlSloKey", ["sloKey"]), a = [i], o = (e, t) => ({
11
+ kind: "binary",
12
+ op: "eq",
13
+ column: e,
14
+ value: t
15
+ }), s = (e) => ({
16
+ nameId: String(e.nameId ?? ""),
17
+ ...e.nameIdFormat ? { nameIdFormat: String(e.nameIdFormat) } : {},
18
+ ...e.sessionIndex ? { sessionIndex: String(e.sessionIndex) } : {}
19
+ }), c = (e, t = 6048e5) => {
20
+ let n = async (t) => (await e.query({
21
+ table: r,
22
+ predicate: o("sloKey", t),
23
+ order: [{
24
+ column: "id",
25
+ direction: "asc"
26
+ }],
27
+ take: 1,
28
+ skip: void 0,
29
+ projection: void 0
30
+ }))[0], i = (e, n) => {
31
+ let r = Number(e.createdAtMs);
32
+ return Number.isFinite(r) && n - r > t;
33
+ }, a = async (t) => {
34
+ await e.delete(r, String(t.id)).catch(() => void 0);
35
+ };
36
+ return {
37
+ save: async (t) => {
38
+ let i = await n(t.sloKey);
39
+ i && await a(i), await e.insert(r, {
40
+ sloKey: t.sloKey,
41
+ nameId: t.nameId,
42
+ nameIdFormat: t.nameIdFormat ?? "",
43
+ sessionIndex: t.sessionIndex ?? "",
44
+ createdAtMs: String(Date.now())
45
+ });
46
+ },
47
+ load: async (e) => {
48
+ let t = await n(e);
49
+ return t ? i(t, Date.now()) ? (await a(t), null) : s(t) : null;
50
+ },
51
+ remove: async (e) => {
52
+ let t = await n(e);
53
+ t && await a(t);
54
+ }
55
+ };
56
+ }, l = (e = 6048e5) => {
57
+ let t = /* @__PURE__ */ new Map();
58
+ return {
59
+ save: async (e) => {
60
+ t.set(e.sloKey, {
61
+ subject: {
62
+ nameId: e.nameId,
63
+ ...e.nameIdFormat ? { nameIdFormat: e.nameIdFormat } : {},
64
+ ...e.sessionIndex ? { sessionIndex: e.sessionIndex } : {}
65
+ },
66
+ createdAt: Date.now()
67
+ });
68
+ },
69
+ load: async (n) => {
70
+ let r = t.get(n);
71
+ return r ? Date.now() - r.createdAt > e ? (t.delete(n), null) : r.subject : null;
72
+ },
73
+ remove: async (e) => {
74
+ t.delete(e);
75
+ }
76
+ };
77
+ };
78
+ //#endregion
79
+ export { r as LOGOUT_SESSION_TABLE, c as dataStoreLogoutStore, l as memoryLogoutStore, i as samlLogoutTable, a as samlLogoutTables };
@@ -0,0 +1,66 @@
1
+ import { SamlMetadataFetchError as e, SamlMetadataParseError as t } from "./saml-errors.js";
2
+ import { Effect as n } from "effect";
3
+ import { HttpClient as r } from "effect/http";
4
+ //#region src/saml-metadata.ts
5
+ var i = "urn:oasis:names:tc:SAML:2.0:metadata", a = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", o = (e) => e.slice(e.indexOf(":") + 1), s = (e, t) => (RegExp(`\\b${t}\\s*=\\s*"([^"]*)"`).exec(e) ?? RegExp(`\\b${t}\\s*=\\s*'([^']*)'`).exec(e))?.[1], c = (e, t) => {
6
+ let n = [], r = /<([A-Za-z][\w.:-]*)\b([^>]*)>/g, i;
7
+ for (; (i = r.exec(e)) !== null;) o(i[1]) === t && n.push(i[0]);
8
+ return n;
9
+ }, l = (e) => {
10
+ let t = [], n = /<([A-Za-z][\w.:-]*KeyDescriptor)\b([^>]*)>/g, r;
11
+ for (; (r = n.exec(e)) !== null;) {
12
+ if (o(r[1]) !== "KeyDescriptor") continue;
13
+ let n = RegExp(`</${r[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}>`), i = e.slice(r.index + r[0].length), a = n.exec(i), c = a ? i.slice(0, a.index) : i;
14
+ t.push({
15
+ use: s(r[0], "use"),
16
+ body: c
17
+ });
18
+ }
19
+ return t;
20
+ }, u = (e) => {
21
+ let t = l(e), n = t.find((e) => e.use === "signing") ?? t.find((e) => e.use === void 0) ?? t[0];
22
+ if (!n) return;
23
+ let r = /<[^>]*X509Certificate[^>]*>([\s\S]*?)<\/[^>]*X509Certificate>/.exec(n.body)?.[1]?.replace(/\s+/g, "");
24
+ return r && r.length > 0 ? r : void 0;
25
+ }, d = (e, r) => n.gen(function* () {
26
+ if (!e.includes(i) && !/EntityDescriptor/.test(e)) return yield* n.fail(new t({
27
+ url: r,
28
+ reason: "not-saml-metadata"
29
+ }));
30
+ let o = c(e, "SingleSignOnService"), l = o.find((e) => s(e, "Binding") === a) ?? o[0], d = l ? s(l, "Location") : void 0;
31
+ if (!d) return yield* n.fail(new t({
32
+ url: r,
33
+ reason: "no-entryPoint"
34
+ }));
35
+ let f = u(e);
36
+ if (!f) return yield* n.fail(new t({
37
+ url: r,
38
+ reason: "no-signing-cert"
39
+ }));
40
+ let p = c(e, "SingleLogoutService"), m = p.find((e) => s(e, "Binding") === a) ?? p[0], h = m ? s(m, "Location") : void 0, g = c(e, "EntityDescriptor")[0], _ = g ? s(g, "entityID") : void 0;
41
+ return {
42
+ entryPoint: d,
43
+ idpCert: f,
44
+ ...h ? { logoutUrl: h } : {},
45
+ ..._ ? { issuer: _ } : {}
46
+ };
47
+ }), f = (t) => n.gen(function* () {
48
+ let i = yield* (yield* r.HttpClient).get(t).pipe(n.mapError(() => new e({
49
+ url: t,
50
+ reason: "network"
51
+ })));
52
+ if (i.status < 200 || i.status >= 300) return yield* n.fail(new e({
53
+ url: t,
54
+ reason: `status ${i.status}`
55
+ }));
56
+ let a = yield* i.text.pipe(n.mapError(() => new e({
57
+ url: t,
58
+ reason: "body"
59
+ })));
60
+ return a.trim() ? yield* d(a, t) : yield* n.fail(new e({
61
+ url: t,
62
+ reason: "empty-body"
63
+ }));
64
+ });
65
+ //#endregion
66
+ export { f as fetchIdpMetadata, d as parseIdpMetadata };
package/dist/saml.js ADDED
@@ -0,0 +1,34 @@
1
+ //#region src/saml.ts
2
+ var e = (e) => e.replace(/-----(BEGIN|END) CERTIFICATE-----/g, "").replace(/\s+/g, ""), t = (t) => {
3
+ let r = t.signingCert ? `
4
+ <KeyDescriptor use="signing">
5
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
6
+ <X509Data><X509Certificate>${n(e(t.signingCert))}</X509Certificate></X509Data>
7
+ </KeyInfo>
8
+ </KeyDescriptor>` : "", i = t.sloUrl ? `
9
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="${n(t.sloUrl)}"/>` : "";
10
+ return `<?xml version="1.0"?>
11
+ <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="${n(t.entityId)}">
12
+ <SPSSODescriptor AuthnRequestsSigned="${t.authnRequestsSigned ? "true" : "false"}" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">${r}${i}
13
+ <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress</NameIDFormat>
14
+ <AssertionConsumerService index="0" isDefault="true" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${n(t.acsUrl)}"/>
15
+ </SPSSODescriptor>
16
+ </EntityDescriptor>`;
17
+ }, n = (e) => e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"), r = (e) => {
18
+ let t = e.attributes ?? {}, n = (...n) => {
19
+ for (let r of n) {
20
+ let n = e[r] ?? t[r];
21
+ if (typeof n == "string" && n.length > 0) return n;
22
+ if (Array.isArray(n) && typeof n[0] == "string") return n[0];
23
+ }
24
+ return null;
25
+ }, r = String(e.nameID ?? e.nameId ?? n("nameID") ?? "");
26
+ return {
27
+ nameId: r,
28
+ email: n("email", "mail", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress") ?? (r.includes("@") ? r : null),
29
+ displayName: n("displayName", "name", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "cn"),
30
+ attributes: t
31
+ };
32
+ };
33
+ //#endregion
34
+ export { r as extractProfile, t as spMetadataXml };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-sso-saml",
3
- "version": "0.84.0",
3
+ "version": "0.86.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",
@@ -33,15 +33,14 @@
33
33
  "node": ">=24.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "@effect/platform": "^0.97.2",
37
- "@voltro/database": "0.84.0",
38
- "@voltro/protocol": "0.84.0"
36
+ "@voltro/database": "0.86.0",
37
+ "@voltro/protocol": "0.86.0"
39
38
  },
40
39
  "optionalDependencies": {
41
40
  "@node-saml/node-saml": "^5.1.0"
42
41
  },
43
42
  "peerDependencies": {
44
- "effect": "^3.22.2"
43
+ "effect": "4.0.1"
45
44
  },
46
45
  "publishConfig": {
47
46
  "access": "public"