@hanzo/iam 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.cjs +111 -0
- package/dist/auth.cjs.map +1 -0
- package/dist/auth.d.cts +19 -0
- package/dist/auth.d.ts +7 -4
- package/dist/auth.js +94 -121
- package/dist/auth.js.map +1 -1
- package/dist/betterauth.cjs +34 -0
- package/dist/betterauth.cjs.map +1 -0
- package/dist/betterauth.d.cts +64 -0
- package/dist/betterauth.d.ts +7 -10
- package/dist/betterauth.js +28 -62
- package/dist/betterauth.js.map +1 -1
- package/dist/billing.cjs +8 -0
- package/dist/billing.cjs.map +1 -0
- package/dist/billing.d.cts +2 -0
- package/dist/billing.d.ts +2 -16
- package/dist/billing.js +5 -17
- package/dist/billing.js.map +1 -1
- package/dist/browser.cjs +680 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +217 -0
- package/dist/browser.d.ts +10 -7
- package/dist/browser.js +645 -663
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +1087 -0
- package/dist/index.cjs.map +1 -0
- package/dist/{client.d.ts → index.d.cts} +23 -4
- package/dist/index.d.ts +86 -23
- package/dist/index.js +1077 -29
- package/dist/index.js.map +1 -1
- package/dist/nextauth.cjs +35 -0
- package/dist/nextauth.cjs.map +1 -0
- package/dist/nextauth.d.cts +55 -0
- package/dist/nextauth.d.ts +4 -7
- package/dist/nextauth.js +30 -66
- package/dist/nextauth.js.map +1 -1
- package/dist/passport.cjs +49 -0
- package/dist/passport.cjs.map +1 -0
- package/dist/passport.d.cts +47 -0
- package/dist/passport.d.ts +8 -5
- package/dist/passport.js +45 -65
- package/dist/passport.js.map +1 -1
- package/dist/react.cjs +1434 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +133 -0
- package/dist/react.d.ts +18 -50
- package/dist/react.js +1399 -494
- package/dist/react.js.map +1 -1
- package/dist/types.cjs +4 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +219 -0
- package/dist/types.d.ts +25 -24
- package/dist/types.js +2 -5
- package/dist/types.js.map +1 -1
- package/package.json +24 -13
- package/dist/auth.d.ts.map +0 -1
- package/dist/betterauth.d.ts.map +0 -1
- package/dist/billing.d.ts.map +0 -1
- package/dist/browser.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -292
- package/dist/client.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/nextauth.d.ts.map +0 -1
- package/dist/passport.d.ts.map +0 -1
- package/dist/pkce.d.ts +0 -13
- package/dist/pkce.d.ts.map +0 -1
- package/dist/pkce.js +0 -36
- package/dist/pkce.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
package/dist/auth.cjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jose = require('jose');
|
|
4
|
+
|
|
5
|
+
// src/auth.ts
|
|
6
|
+
var jwksSets = /* @__PURE__ */ new Map();
|
|
7
|
+
function getJwksKeySet(jwksUri) {
|
|
8
|
+
let keySet = jwksSets.get(jwksUri);
|
|
9
|
+
if (!keySet) {
|
|
10
|
+
keySet = jose.createRemoteJWKSet(new URL(jwksUri));
|
|
11
|
+
jwksSets.set(jwksUri, keySet);
|
|
12
|
+
}
|
|
13
|
+
return keySet;
|
|
14
|
+
}
|
|
15
|
+
function clearJwksCache() {
|
|
16
|
+
jwksSets.clear();
|
|
17
|
+
}
|
|
18
|
+
var discoveryCache = /* @__PURE__ */ new Map();
|
|
19
|
+
var DISCOVERY_TTL_MS = 5 * 60 * 1e3;
|
|
20
|
+
async function resolveJwksUri(serverUrl) {
|
|
21
|
+
const baseUrl = serverUrl.replace(/\/+$/, "");
|
|
22
|
+
const cached = discoveryCache.get(baseUrl);
|
|
23
|
+
if (cached && Date.now() - cached.fetchedAt < DISCOVERY_TTL_MS) {
|
|
24
|
+
return { jwksUri: cached.jwksUri, issuer: cached.issuer };
|
|
25
|
+
}
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timer = setTimeout(() => controller.abort(), 8e3);
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`${baseUrl}/.well-known/openid-configuration`, {
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
headers: { Accept: "application/json" }
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
throw new Error(`OIDC discovery failed: ${res.status}`);
|
|
35
|
+
}
|
|
36
|
+
const body = await res.json();
|
|
37
|
+
const jwksUri = body.jwks_uri;
|
|
38
|
+
const issuer = body.issuer ?? baseUrl;
|
|
39
|
+
if (!jwksUri) {
|
|
40
|
+
throw new Error("OIDC discovery response missing jwks_uri");
|
|
41
|
+
}
|
|
42
|
+
discoveryCache.set(baseUrl, { jwksUri, issuer, fetchedAt: Date.now() });
|
|
43
|
+
return { jwksUri, issuer };
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function validateToken(token, config) {
|
|
49
|
+
if (!token || typeof token !== "string") {
|
|
50
|
+
return { ok: false, reason: "iam_token_missing" };
|
|
51
|
+
}
|
|
52
|
+
let jwksUri;
|
|
53
|
+
let issuer;
|
|
54
|
+
try {
|
|
55
|
+
const discovery = await resolveJwksUri(config.serverUrl);
|
|
56
|
+
jwksUri = discovery.jwksUri;
|
|
57
|
+
issuer = discovery.issuer;
|
|
58
|
+
} catch {
|
|
59
|
+
return { ok: false, reason: "iam_discovery_failed" };
|
|
60
|
+
}
|
|
61
|
+
const keySet = getJwksKeySet(jwksUri);
|
|
62
|
+
let payload;
|
|
63
|
+
try {
|
|
64
|
+
const result = await jose.jwtVerify(token, keySet, {
|
|
65
|
+
issuer,
|
|
66
|
+
audience: config.clientId,
|
|
67
|
+
clockTolerance: 30
|
|
68
|
+
// 30s clock skew
|
|
69
|
+
});
|
|
70
|
+
payload = result.payload;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
if (message.includes("expired")) {
|
|
74
|
+
return { ok: false, reason: "iam_token_expired" };
|
|
75
|
+
}
|
|
76
|
+
if (message.includes("audience")) {
|
|
77
|
+
try {
|
|
78
|
+
const result = await jose.jwtVerify(token, keySet, {
|
|
79
|
+
issuer,
|
|
80
|
+
clockTolerance: 30
|
|
81
|
+
});
|
|
82
|
+
payload = result.payload;
|
|
83
|
+
} catch {
|
|
84
|
+
return { ok: false, reason: "iam_signature_invalid" };
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
return { ok: false, reason: "iam_signature_invalid" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const claims = payload;
|
|
91
|
+
const sub = claims.sub || (typeof claims.owner === "string" && typeof claims.name === "string" ? `${claims.owner}/${claims.name}` : void 0);
|
|
92
|
+
if (!sub) {
|
|
93
|
+
return { ok: false, reason: "iam_subject_missing" };
|
|
94
|
+
}
|
|
95
|
+
const parts = sub.split("/");
|
|
96
|
+
const owner = parts.length > 1 ? parts[0] : config.orgName ?? "unknown";
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
userId: sub,
|
|
100
|
+
email: typeof claims.email === "string" ? claims.email : void 0,
|
|
101
|
+
name: typeof claims.name === "string" ? claims.name : typeof claims.preferred_username === "string" ? claims.preferred_username : void 0,
|
|
102
|
+
avatar: typeof claims.picture === "string" ? claims.picture : void 0,
|
|
103
|
+
owner,
|
|
104
|
+
claims
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
exports.clearJwksCache = clearJwksCache;
|
|
109
|
+
exports.validateToken = validateToken;
|
|
110
|
+
//# sourceMappingURL=auth.cjs.map
|
|
111
|
+
//# sourceMappingURL=auth.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth.ts"],"names":["createRemoteJWKSet","jwtVerify"],"mappings":";;;;;AAaA,IAAM,QAAA,uBAAe,GAAA,EAAmD;AAExE,SAAS,cAAc,OAAA,EAAwD;AAC7E,EAAA,IAAI,MAAA,GAAS,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA;AACjC,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAA,GAASA,uBAAA,CAAmB,IAAI,GAAA,CAAI,OAAO,CAAC,CAAA;AAC5C,IAAA,QAAA,CAAS,GAAA,CAAI,SAAS,MAAM,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,MAAA;AACT;AAGO,SAAS,cAAA,GAAuB;AACrC,EAAA,QAAA,CAAS,KAAA,EAAM;AACjB;AAOA,IAAM,cAAA,uBAAqB,GAAA,EAA6B;AACxD,IAAM,gBAAA,GAAmB,IAAI,EAAA,GAAK,GAAA;AAElC,eAAe,eAAe,SAAA,EAAiE;AAC7F,EAAA,MAAM,OAAA,GAAU,SAAA,CAAU,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AAC5C,EAAA,MAAM,MAAA,GAAS,cAAA,CAAe,GAAA,CAAI,OAAO,CAAA;AACzC,EAAA,IAAI,UAAU,IAAA,CAAK,GAAA,EAAI,GAAI,MAAA,CAAO,YAAY,gBAAA,EAAkB;AAC9D,IAAA,OAAO,EAAE,OAAA,EAAS,MAAA,CAAO,OAAA,EAAS,MAAA,EAAQ,OAAO,MAAA,EAAO;AAAA,EAC1D;AAEA,EAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,EAAA,MAAM,QAAQ,UAAA,CAAW,MAAM,UAAA,CAAW,KAAA,IAAS,GAAK,CAAA;AACxD,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,EAAG,OAAO,CAAA,iCAAA,CAAA,EAAqC;AAAA,MACrE,QAAQ,UAAA,CAAW,MAAA;AAAA,MACnB,OAAA,EAAS,EAAE,MAAA,EAAQ,kBAAA;AAAmB,KACvC,CAAA;AACD,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAAA,IACxD;AACA,IAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,IAAA,MAAM,UAAU,IAAA,CAAK,QAAA;AACrB,IAAA,MAAM,MAAA,GAAS,KAAK,MAAA,IAAU,OAAA;AAC9B,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,MAAM,IAAI,MAAM,0CAA0C,CAAA;AAAA,IAC5D;AACA,IAAA,cAAA,CAAe,GAAA,CAAI,SAAS,EAAE,OAAA,EAAS,QAAQ,SAAA,EAAW,IAAA,CAAK,GAAA,EAAI,EAAG,CAAA;AACtE,IAAA,OAAO,EAAE,SAAS,MAAA,EAAO;AAAA,EAC3B,CAAA,SAAE;AACA,IAAA,YAAA,CAAa,KAAK,CAAA;AAAA,EACpB;AACF;AAYA,eAAsB,aAAA,CACpB,OACA,MAAA,EACwB;AACxB,EAAA,IAAI,CAAC,KAAA,IAAS,OAAO,KAAA,KAAU,QAAA,EAAU;AACvC,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,mBAAA,EAAoB;AAAA,EAClD;AAEA,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAM,SAAA,GAAY,MAAM,cAAA,CAAe,MAAA,CAAO,SAAS,CAAA;AACvD,IAAA,OAAA,GAAU,SAAA,CAAU,OAAA;AACpB,IAAA,MAAA,GAAS,SAAA,CAAU,MAAA;AAAA,EACrB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,sBAAA,EAAuB;AAAA,EACrD;AAEA,EAAA,MAAM,MAAA,GAAS,cAAc,OAAO,CAAA;AAEpC,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,MAAMC,cAAA,CAAU,KAAA,EAAO,MAAA,EAAQ;AAAA,MAC5C,MAAA;AAAA,MACA,UAAU,MAAA,CAAO,QAAA;AAAA,MACjB,cAAA,EAAgB;AAAA;AAAA,KACjB,CAAA;AACD,IAAA,OAAA,GAAU,MAAA,CAAO,OAAA;AAAA,EACnB,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,IAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,SAAS,CAAA,EAAG;AAC/B,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,mBAAA,EAAoB;AAAA,IAClD;AACA,IAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,UAAU,CAAA,EAAG;AAEhC,MAAA,IAAI;AACF,QAAA,MAAM,MAAA,GAAS,MAAMA,cAAA,CAAU,KAAA,EAAO,MAAA,EAAQ;AAAA,UAC5C,MAAA;AAAA,UACA,cAAA,EAAgB;AAAA,SACjB,CAAA;AACD,QAAA,OAAA,GAAU,MAAA,CAAO,OAAA;AAAA,MACnB,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,uBAAA,EAAwB;AAAA,MACtD;AAAA,IACF,CAAA,MAAO;AACL,MAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,uBAAA,EAAwB;AAAA,IACtD;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAAS,OAAA;AAGf,EAAA,MAAM,MACJ,MAAA,CAAO,GAAA,KACN,OAAO,MAAA,CAAO,UAAU,QAAA,IAAY,OAAO,MAAA,CAAO,IAAA,KAAS,WACxD,CAAA,EAAG,MAAA,CAAO,KAAK,CAAA,CAAA,EAAI,MAAA,CAAO,IAAI,CAAA,CAAA,GAC9B,MAAA,CAAA;AAEN,EAAA,IAAI,CAAC,GAAA,EAAK;AACR,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,qBAAA,EAAsB;AAAA,EACpD;AAGA,EAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA;AAC3B,EAAA,MAAM,KAAA,GAAQ,MAAM,MAAA,GAAS,CAAA,GAAI,MAAM,CAAC,CAAA,GAAI,OAAO,OAAA,IAAW,SAAA;AAE9D,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,IAAA;AAAA,IACJ,MAAA,EAAQ,GAAA;AAAA,IACR,OAAO,OAAO,MAAA,CAAO,KAAA,KAAU,QAAA,GAAW,OAAO,KAAA,GAAQ,MAAA;AAAA,IACzD,IAAA,EACE,OAAO,MAAA,CAAO,IAAA,KAAS,QAAA,GACnB,MAAA,CAAO,IAAA,GACP,OAAO,MAAA,CAAO,kBAAA,KAAuB,QAAA,GACnC,MAAA,CAAO,kBAAA,GACP,MAAA;AAAA,IACR,QAAQ,OAAO,MAAA,CAAO,OAAA,KAAY,QAAA,GAAW,OAAO,OAAA,GAAU,MAAA;AAAA,IAC9D,KAAA;AAAA,IACA;AAAA,GACF;AACF","file":"auth.cjs","sourcesContent":["/**\n * JWT validation using jose library + OIDC JWKS discovery.\n *\n * Validates access/ID tokens issued by Hanzo IAM.\n */\n\nimport { createRemoteJWKSet, jwtVerify, type JWTPayload } from \"jose\";\nimport type { IamConfig, IamAuthResult, IamJwtClaims } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// JWKS key set cache (per issuer)\n// ---------------------------------------------------------------------------\n\nconst jwksSets = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\nfunction getJwksKeySet(jwksUri: string): ReturnType<typeof createRemoteJWKSet> {\n let keySet = jwksSets.get(jwksUri);\n if (!keySet) {\n keySet = createRemoteJWKSet(new URL(jwksUri));\n jwksSets.set(jwksUri, keySet);\n }\n return keySet;\n}\n\n/** Clear cached JWKS key sets (useful for testing or key rotation). */\nexport function clearJwksCache(): void {\n jwksSets.clear();\n}\n\n// ---------------------------------------------------------------------------\n// OIDC discovery cache (lightweight, no full client needed)\n// ---------------------------------------------------------------------------\n\ntype CachedDiscovery = { jwksUri: string; issuer: string; fetchedAt: number };\nconst discoveryCache = new Map<string, CachedDiscovery>();\nconst DISCOVERY_TTL_MS = 5 * 60 * 1000;\n\nasync function resolveJwksUri(serverUrl: string): Promise<{ jwksUri: string; issuer: string }> {\n const baseUrl = serverUrl.replace(/\\/+$/, \"\");\n const cached = discoveryCache.get(baseUrl);\n if (cached && Date.now() - cached.fetchedAt < DISCOVERY_TTL_MS) {\n return { jwksUri: cached.jwksUri, issuer: cached.issuer };\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 8_000);\n try {\n const res = await fetch(`${baseUrl}/.well-known/openid-configuration`, {\n signal: controller.signal,\n headers: { Accept: \"application/json\" },\n });\n if (!res.ok) {\n throw new Error(`OIDC discovery failed: ${res.status}`);\n }\n const body = (await res.json()) as { jwks_uri?: string; issuer?: string };\n const jwksUri = body.jwks_uri;\n const issuer = body.issuer ?? baseUrl;\n if (!jwksUri) {\n throw new Error(\"OIDC discovery response missing jwks_uri\");\n }\n discoveryCache.set(baseUrl, { jwksUri, issuer, fetchedAt: Date.now() });\n return { jwksUri, issuer };\n } finally {\n clearTimeout(timer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Token validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a JWT access token against IAM's JWKS.\n *\n * Uses OIDC discovery to find the JWKS URI, then verifies the token\n * signature, issuer, audience, and expiry using the `jose` library.\n */\nexport async function validateToken(\n token: string,\n config: IamConfig,\n): Promise<IamAuthResult> {\n if (!token || typeof token !== \"string\") {\n return { ok: false, reason: \"iam_token_missing\" };\n }\n\n let jwksUri: string;\n let issuer: string;\n try {\n const discovery = await resolveJwksUri(config.serverUrl);\n jwksUri = discovery.jwksUri;\n issuer = discovery.issuer;\n } catch {\n return { ok: false, reason: \"iam_discovery_failed\" };\n }\n\n const keySet = getJwksKeySet(jwksUri);\n\n let payload: JWTPayload;\n try {\n const result = await jwtVerify(token, keySet, {\n issuer,\n audience: config.clientId,\n clockTolerance: 30, // 30s clock skew\n });\n payload = result.payload;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes(\"expired\")) {\n return { ok: false, reason: \"iam_token_expired\" };\n }\n if (message.includes(\"audience\")) {\n // Retry without audience check - some IAM configs don't set aud\n try {\n const result = await jwtVerify(token, keySet, {\n issuer,\n clockTolerance: 30,\n });\n payload = result.payload;\n } catch {\n return { ok: false, reason: \"iam_signature_invalid\" };\n }\n } else {\n return { ok: false, reason: \"iam_signature_invalid\" };\n }\n }\n\n const claims = payload as unknown as IamJwtClaims;\n\n // Hanzo IAM tokens may use owner/name instead of sub claim\n const sub =\n claims.sub ||\n (typeof claims.owner === \"string\" && typeof claims.name === \"string\"\n ? `${claims.owner}/${claims.name}`\n : undefined);\n\n if (!sub) {\n return { ok: false, reason: \"iam_subject_missing\" };\n }\n\n // IAM sub format is \"org/username\" - extract owner\n const parts = sub.split(\"/\");\n const owner = parts.length > 1 ? parts[0] : config.orgName ?? \"unknown\";\n\n return {\n ok: true,\n userId: sub,\n email: typeof claims.email === \"string\" ? claims.email : undefined,\n name:\n typeof claims.name === \"string\"\n ? claims.name\n : typeof claims.preferred_username === \"string\"\n ? claims.preferred_username\n : undefined,\n avatar: typeof claims.picture === \"string\" ? claims.picture : undefined,\n owner,\n claims,\n };\n}\n"]}
|
package/dist/auth.d.cts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { IamConfig, IamAuthResult } from './types.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JWT validation using jose library + OIDC JWKS discovery.
|
|
5
|
+
*
|
|
6
|
+
* Validates access/ID tokens issued by Hanzo IAM.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Clear cached JWKS key sets (useful for testing or key rotation). */
|
|
10
|
+
declare function clearJwksCache(): void;
|
|
11
|
+
/**
|
|
12
|
+
* Validate a JWT access token against IAM's JWKS.
|
|
13
|
+
*
|
|
14
|
+
* Uses OIDC discovery to find the JWKS URI, then verifies the token
|
|
15
|
+
* signature, issuer, audience, and expiry using the `jose` library.
|
|
16
|
+
*/
|
|
17
|
+
declare function validateToken(token: string, config: IamConfig): Promise<IamAuthResult>;
|
|
18
|
+
|
|
19
|
+
export { clearJwksCache, validateToken };
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
import { IamConfig, IamAuthResult } from './types.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* JWT validation using jose library + OIDC JWKS discovery.
|
|
3
5
|
*
|
|
4
6
|
* Validates access/ID tokens issued by Hanzo IAM.
|
|
5
7
|
*/
|
|
6
|
-
|
|
8
|
+
|
|
7
9
|
/** Clear cached JWKS key sets (useful for testing or key rotation). */
|
|
8
|
-
|
|
10
|
+
declare function clearJwksCache(): void;
|
|
9
11
|
/**
|
|
10
12
|
* Validate a JWT access token against IAM's JWKS.
|
|
11
13
|
*
|
|
12
14
|
* Uses OIDC discovery to find the JWKS URI, then verifies the token
|
|
13
15
|
* signature, issuer, audience, and expiry using the `jose` library.
|
|
14
16
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
declare function validateToken(token: string, config: IamConfig): Promise<IamAuthResult>;
|
|
18
|
+
|
|
19
|
+
export { clearJwksCache, validateToken };
|
package/dist/auth.js
CHANGED
|
@@ -1,135 +1,108 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*/
|
|
6
|
-
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// JWKS key set cache (per issuer)
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
const jwksSets = new Map();
|
|
1
|
+
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
|
2
|
+
|
|
3
|
+
// src/auth.ts
|
|
4
|
+
var jwksSets = /* @__PURE__ */ new Map();
|
|
11
5
|
function getJwksKeySet(jwksUri) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
6
|
+
let keySet = jwksSets.get(jwksUri);
|
|
7
|
+
if (!keySet) {
|
|
8
|
+
keySet = createRemoteJWKSet(new URL(jwksUri));
|
|
9
|
+
jwksSets.set(jwksUri, keySet);
|
|
10
|
+
}
|
|
11
|
+
return keySet;
|
|
18
12
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
jwksSets.clear();
|
|
13
|
+
function clearJwksCache() {
|
|
14
|
+
jwksSets.clear();
|
|
22
15
|
}
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
var discoveryCache = /* @__PURE__ */ new Map();
|
|
17
|
+
var DISCOVERY_TTL_MS = 5 * 60 * 1e3;
|
|
25
18
|
async function resolveJwksUri(serverUrl) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
const body = (await res.json());
|
|
42
|
-
const jwksUri = body.jwks_uri;
|
|
43
|
-
const issuer = body.issuer ?? baseUrl;
|
|
44
|
-
if (!jwksUri) {
|
|
45
|
-
throw new Error("OIDC discovery response missing jwks_uri");
|
|
46
|
-
}
|
|
47
|
-
discoveryCache.set(baseUrl, { jwksUri, issuer, fetchedAt: Date.now() });
|
|
48
|
-
return { jwksUri, issuer };
|
|
19
|
+
const baseUrl = serverUrl.replace(/\/+$/, "");
|
|
20
|
+
const cached = discoveryCache.get(baseUrl);
|
|
21
|
+
if (cached && Date.now() - cached.fetchedAt < DISCOVERY_TTL_MS) {
|
|
22
|
+
return { jwksUri: cached.jwksUri, issuer: cached.issuer };
|
|
23
|
+
}
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), 8e3);
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`${baseUrl}/.well-known/openid-configuration`, {
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
headers: { Accept: "application/json" }
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`OIDC discovery failed: ${res.status}`);
|
|
49
33
|
}
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
const body = await res.json();
|
|
35
|
+
const jwksUri = body.jwks_uri;
|
|
36
|
+
const issuer = body.issuer ?? baseUrl;
|
|
37
|
+
if (!jwksUri) {
|
|
38
|
+
throw new Error("OIDC discovery response missing jwks_uri");
|
|
52
39
|
}
|
|
40
|
+
discoveryCache.set(baseUrl, { jwksUri, issuer, fetchedAt: Date.now() });
|
|
41
|
+
return { jwksUri, issuer };
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
53
45
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
46
|
+
async function validateToken(token, config) {
|
|
47
|
+
if (!token || typeof token !== "string") {
|
|
48
|
+
return { ok: false, reason: "iam_token_missing" };
|
|
49
|
+
}
|
|
50
|
+
let jwksUri;
|
|
51
|
+
let issuer;
|
|
52
|
+
try {
|
|
53
|
+
const discovery = await resolveJwksUri(config.serverUrl);
|
|
54
|
+
jwksUri = discovery.jwksUri;
|
|
55
|
+
issuer = discovery.issuer;
|
|
56
|
+
} catch {
|
|
57
|
+
return { ok: false, reason: "iam_discovery_failed" };
|
|
58
|
+
}
|
|
59
|
+
const keySet = getJwksKeySet(jwksUri);
|
|
60
|
+
let payload;
|
|
61
|
+
try {
|
|
62
|
+
const result = await jwtVerify(token, keySet, {
|
|
63
|
+
issuer,
|
|
64
|
+
audience: config.clientId,
|
|
65
|
+
clockTolerance: 30
|
|
66
|
+
// 30s clock skew
|
|
67
|
+
});
|
|
68
|
+
payload = result.payload;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
+
if (message.includes("expired")) {
|
|
72
|
+
return { ok: false, reason: "iam_token_expired" };
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
const keySet = getJwksKeySet(jwksUri);
|
|
78
|
-
let payload;
|
|
79
|
-
try {
|
|
74
|
+
if (message.includes("audience")) {
|
|
75
|
+
try {
|
|
80
76
|
const result = await jwtVerify(token, keySet, {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
clockTolerance: 30, // 30s clock skew
|
|
77
|
+
issuer,
|
|
78
|
+
clockTolerance: 30
|
|
84
79
|
});
|
|
85
80
|
payload = result.payload;
|
|
81
|
+
} catch {
|
|
82
|
+
return { ok: false, reason: "iam_signature_invalid" };
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
return { ok: false, reason: "iam_signature_invalid" };
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
return { ok: false, reason: "iam_signature_invalid" };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const claims = payload;
|
|
110
|
-
// Hanzo IAM tokens may use owner/name instead of sub claim
|
|
111
|
-
const sub = claims.sub ||
|
|
112
|
-
(typeof claims.owner === "string" && typeof claims.name === "string"
|
|
113
|
-
? `${claims.owner}/${claims.name}`
|
|
114
|
-
: undefined);
|
|
115
|
-
if (!sub) {
|
|
116
|
-
return { ok: false, reason: "iam_subject_missing" };
|
|
117
|
-
}
|
|
118
|
-
// IAM sub format is "org/username" - extract owner
|
|
119
|
-
const parts = sub.split("/");
|
|
120
|
-
const owner = parts.length > 1 ? parts[0] : config.orgName ?? "unknown";
|
|
121
|
-
return {
|
|
122
|
-
ok: true,
|
|
123
|
-
userId: sub,
|
|
124
|
-
email: typeof claims.email === "string" ? claims.email : undefined,
|
|
125
|
-
name: typeof claims.name === "string"
|
|
126
|
-
? claims.name
|
|
127
|
-
: typeof claims.preferred_username === "string"
|
|
128
|
-
? claims.preferred_username
|
|
129
|
-
: undefined,
|
|
130
|
-
avatar: typeof claims.picture === "string" ? claims.picture : undefined,
|
|
131
|
-
owner,
|
|
132
|
-
claims,
|
|
133
|
-
};
|
|
87
|
+
}
|
|
88
|
+
const claims = payload;
|
|
89
|
+
const sub = claims.sub || (typeof claims.owner === "string" && typeof claims.name === "string" ? `${claims.owner}/${claims.name}` : void 0);
|
|
90
|
+
if (!sub) {
|
|
91
|
+
return { ok: false, reason: "iam_subject_missing" };
|
|
92
|
+
}
|
|
93
|
+
const parts = sub.split("/");
|
|
94
|
+
const owner = parts.length > 1 ? parts[0] : config.orgName ?? "unknown";
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
userId: sub,
|
|
98
|
+
email: typeof claims.email === "string" ? claims.email : void 0,
|
|
99
|
+
name: typeof claims.name === "string" ? claims.name : typeof claims.preferred_username === "string" ? claims.preferred_username : void 0,
|
|
100
|
+
avatar: typeof claims.picture === "string" ? claims.picture : void 0,
|
|
101
|
+
owner,
|
|
102
|
+
claims
|
|
103
|
+
};
|
|
134
104
|
}
|
|
105
|
+
|
|
106
|
+
export { clearJwksCache, validateToken };
|
|
107
|
+
//# sourceMappingURL=auth.js.map
|
|
135
108
|
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"
|
|
1
|
+
{"version":3,"sources":["../src/auth.ts"],"names":[],"mappings":"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","file":"auth.js","sourcesContent":["/**\n * JWT validation using jose library + OIDC JWKS discovery.\n *\n * Validates access/ID tokens issued by Hanzo IAM.\n */\n\nimport { createRemoteJWKSet, jwtVerify, type JWTPayload } from \"jose\";\nimport type { IamConfig, IamAuthResult, IamJwtClaims } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// JWKS key set cache (per issuer)\n// ---------------------------------------------------------------------------\n\nconst jwksSets = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\nfunction getJwksKeySet(jwksUri: string): ReturnType<typeof createRemoteJWKSet> {\n let keySet = jwksSets.get(jwksUri);\n if (!keySet) {\n keySet = createRemoteJWKSet(new URL(jwksUri));\n jwksSets.set(jwksUri, keySet);\n }\n return keySet;\n}\n\n/** Clear cached JWKS key sets (useful for testing or key rotation). */\nexport function clearJwksCache(): void {\n jwksSets.clear();\n}\n\n// ---------------------------------------------------------------------------\n// OIDC discovery cache (lightweight, no full client needed)\n// ---------------------------------------------------------------------------\n\ntype CachedDiscovery = { jwksUri: string; issuer: string; fetchedAt: number };\nconst discoveryCache = new Map<string, CachedDiscovery>();\nconst DISCOVERY_TTL_MS = 5 * 60 * 1000;\n\nasync function resolveJwksUri(serverUrl: string): Promise<{ jwksUri: string; issuer: string }> {\n const baseUrl = serverUrl.replace(/\\/+$/, \"\");\n const cached = discoveryCache.get(baseUrl);\n if (cached && Date.now() - cached.fetchedAt < DISCOVERY_TTL_MS) {\n return { jwksUri: cached.jwksUri, issuer: cached.issuer };\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 8_000);\n try {\n const res = await fetch(`${baseUrl}/.well-known/openid-configuration`, {\n signal: controller.signal,\n headers: { Accept: \"application/json\" },\n });\n if (!res.ok) {\n throw new Error(`OIDC discovery failed: ${res.status}`);\n }\n const body = (await res.json()) as { jwks_uri?: string; issuer?: string };\n const jwksUri = body.jwks_uri;\n const issuer = body.issuer ?? baseUrl;\n if (!jwksUri) {\n throw new Error(\"OIDC discovery response missing jwks_uri\");\n }\n discoveryCache.set(baseUrl, { jwksUri, issuer, fetchedAt: Date.now() });\n return { jwksUri, issuer };\n } finally {\n clearTimeout(timer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Token validation\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a JWT access token against IAM's JWKS.\n *\n * Uses OIDC discovery to find the JWKS URI, then verifies the token\n * signature, issuer, audience, and expiry using the `jose` library.\n */\nexport async function validateToken(\n token: string,\n config: IamConfig,\n): Promise<IamAuthResult> {\n if (!token || typeof token !== \"string\") {\n return { ok: false, reason: \"iam_token_missing\" };\n }\n\n let jwksUri: string;\n let issuer: string;\n try {\n const discovery = await resolveJwksUri(config.serverUrl);\n jwksUri = discovery.jwksUri;\n issuer = discovery.issuer;\n } catch {\n return { ok: false, reason: \"iam_discovery_failed\" };\n }\n\n const keySet = getJwksKeySet(jwksUri);\n\n let payload: JWTPayload;\n try {\n const result = await jwtVerify(token, keySet, {\n issuer,\n audience: config.clientId,\n clockTolerance: 30, // 30s clock skew\n });\n payload = result.payload;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes(\"expired\")) {\n return { ok: false, reason: \"iam_token_expired\" };\n }\n if (message.includes(\"audience\")) {\n // Retry without audience check - some IAM configs don't set aud\n try {\n const result = await jwtVerify(token, keySet, {\n issuer,\n clockTolerance: 30,\n });\n payload = result.payload;\n } catch {\n return { ok: false, reason: \"iam_signature_invalid\" };\n }\n } else {\n return { ok: false, reason: \"iam_signature_invalid\" };\n }\n }\n\n const claims = payload as unknown as IamJwtClaims;\n\n // Hanzo IAM tokens may use owner/name instead of sub claim\n const sub =\n claims.sub ||\n (typeof claims.owner === \"string\" && typeof claims.name === \"string\"\n ? `${claims.owner}/${claims.name}`\n : undefined);\n\n if (!sub) {\n return { ok: false, reason: \"iam_subject_missing\" };\n }\n\n // IAM sub format is \"org/username\" - extract owner\n const parts = sub.split(\"/\");\n const owner = parts.length > 1 ? parts[0] : config.orgName ?? \"unknown\";\n\n return {\n ok: true,\n userId: sub,\n email: typeof claims.email === \"string\" ? claims.email : undefined,\n name:\n typeof claims.name === \"string\"\n ? claims.name\n : typeof claims.preferred_username === \"string\"\n ? claims.preferred_username\n : undefined,\n avatar: typeof claims.picture === \"string\" ? claims.picture : undefined,\n owner,\n claims,\n };\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/betterauth.ts
|
|
4
|
+
function iamProvider(config) {
|
|
5
|
+
const baseUrl = config.serverUrl.replace(/\/+$/, "");
|
|
6
|
+
return {
|
|
7
|
+
id: "iam",
|
|
8
|
+
name: "IAM",
|
|
9
|
+
type: "oidc",
|
|
10
|
+
issuer: baseUrl,
|
|
11
|
+
clientId: config.clientId,
|
|
12
|
+
clientSecret: config.clientSecret,
|
|
13
|
+
authorization: {
|
|
14
|
+
url: `${baseUrl}/oauth/authorize`,
|
|
15
|
+
params: { scope: "openid profile email" }
|
|
16
|
+
},
|
|
17
|
+
token: { url: `${baseUrl}/oauth/token` },
|
|
18
|
+
userinfo: { url: `${baseUrl}/oauth/userinfo` },
|
|
19
|
+
profile(profile) {
|
|
20
|
+
return {
|
|
21
|
+
id: profile.sub ?? profile.id ?? "",
|
|
22
|
+
name: profile.displayName ?? profile.name ?? profile.preferred_username ?? "",
|
|
23
|
+
email: profile.email ?? "",
|
|
24
|
+
image: profile.avatar ?? profile.picture ?? null
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
exports.hanzoIamProvider = iamProvider;
|
|
31
|
+
exports.hanzoIamSocialProvider = iamProvider;
|
|
32
|
+
exports.iamProvider = iamProvider;
|
|
33
|
+
//# sourceMappingURL=betterauth.cjs.map
|
|
34
|
+
//# sourceMappingURL=betterauth.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/betterauth.ts"],"names":[],"mappings":";;;AAmDO,SAAS,YACd,MAAA,EACmB;AACnB,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,SAAA,CAAU,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAEnD,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,KAAA;AAAA,IACJ,IAAA,EAAM,KAAA;AAAA,IACN,IAAA,EAAM,MAAA;AAAA,IACN,MAAA,EAAQ,OAAA;AAAA,IACR,UAAU,MAAA,CAAO,QAAA;AAAA,IACjB,cAAc,MAAA,CAAO,YAAA;AAAA,IACrB,aAAA,EAAe;AAAA,MACb,GAAA,EAAK,GAAG,OAAO,CAAA,gBAAA,CAAA;AAAA,MACf,MAAA,EAAQ,EAAE,KAAA,EAAO,sBAAA;AAAuB,KAC1C;AAAA,IACA,KAAA,EAAO,EAAE,GAAA,EAAK,CAAA,EAAG,OAAO,CAAA,YAAA,CAAA,EAAe;AAAA,IACvC,QAAA,EAAU,EAAE,GAAA,EAAK,CAAA,EAAG,OAAO,CAAA,eAAA,CAAA,EAAkB;AAAA,IAC7C,QAAQ,OAAA,EAAkC;AACxC,MAAA,OAAO;AAAA,QACL,EAAA,EAAK,OAAA,CAAQ,GAAA,IAAmB,OAAA,CAAQ,EAAA,IAAiB,EAAA;AAAA,QACzD,MACG,OAAA,CAAQ,WAAA,IACR,OAAA,CAAQ,IAAA,IACR,QAAQ,kBAAA,IACT,EAAA;AAAA,QACF,KAAA,EAAQ,QAAQ,KAAA,IAAoB,EAAA;AAAA,QACpC,KAAA,EAAQ,OAAA,CAAQ,MAAA,IAAsB,OAAA,CAAQ,OAAA,IAAsB;AAAA,OACtE;AAAA,IACF;AAAA,GACF;AACF","file":"betterauth.cjs","sourcesContent":["/**\n * BetterAuth SSO provider configuration for IAM.\n *\n * Returns a provider config object compatible with BetterAuth's\n * `socialProviders` or generic OAuth plugin.\n *\n * @example\n * ```ts\n * import { betterAuth } from \"better-auth\";\n * import { iamProvider } from \"@hanzo/iam/betterauth\";\n *\n * export const auth = betterAuth({\n * socialProviders: [\n * iamProvider({\n * serverUrl: process.env.IAM_SERVER_URL!,\n * clientId: process.env.IAM_CLIENT_ID!,\n * clientSecret: process.env.IAM_CLIENT_SECRET!,\n * }),\n * ],\n * });\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { IamConfig } from \"./types.js\";\n\nexport interface IamSocialProvider {\n id: string;\n name: string;\n type: \"oidc\";\n issuer: string;\n clientId: string;\n clientSecret?: string;\n authorization: { url: string; params: { scope: string } };\n token: { url: string };\n userinfo: { url: string };\n profile: (profile: Record<string, unknown>) => {\n id: string;\n name: string;\n email: string;\n image: string | null;\n };\n}\n\n/**\n * Create a BetterAuth-compatible social provider for IAM.\n *\n * Works with BetterAuth's SSO plugin or generic OAuth integration.\n * Uses standard OIDC endpoints.\n */\nexport function iamProvider(\n config: IamConfig & { redirectUri?: string },\n): IamSocialProvider {\n const baseUrl = config.serverUrl.replace(/\\/+$/, \"\");\n\n return {\n id: \"iam\",\n name: \"IAM\",\n type: \"oidc\",\n issuer: baseUrl,\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n authorization: {\n url: `${baseUrl}/oauth/authorize`,\n params: { scope: \"openid profile email\" },\n },\n token: { url: `${baseUrl}/oauth/token` },\n userinfo: { url: `${baseUrl}/oauth/userinfo` },\n profile(profile: Record<string, unknown>) {\n return {\n id: (profile.sub as string) ?? (profile.id as string) ?? \"\",\n name:\n (profile.displayName as string) ??\n (profile.name as string) ??\n (profile.preferred_username as string) ??\n \"\",\n email: (profile.email as string) ?? \"\",\n image: (profile.avatar as string) ?? (profile.picture as string) ?? null,\n };\n },\n };\n}\n\n// Backwards-compatible aliases\n/** @deprecated Use iamProvider instead */\nexport { iamProvider as hanzoIamProvider };\n/** @deprecated Use iamProvider instead */\nexport { iamProvider as hanzoIamSocialProvider };\n/** @deprecated Use IamSocialProvider instead */\nexport type { IamSocialProvider as HanzoIamSocialProvider };\n"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { IamConfig } from './types.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BetterAuth SSO provider configuration for IAM.
|
|
5
|
+
*
|
|
6
|
+
* Returns a provider config object compatible with BetterAuth's
|
|
7
|
+
* `socialProviders` or generic OAuth plugin.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { betterAuth } from "better-auth";
|
|
12
|
+
* import { iamProvider } from "@hanzo/iam/betterauth";
|
|
13
|
+
*
|
|
14
|
+
* export const auth = betterAuth({
|
|
15
|
+
* socialProviders: [
|
|
16
|
+
* iamProvider({
|
|
17
|
+
* serverUrl: process.env.IAM_SERVER_URL!,
|
|
18
|
+
* clientId: process.env.IAM_CLIENT_ID!,
|
|
19
|
+
* clientSecret: process.env.IAM_CLIENT_SECRET!,
|
|
20
|
+
* }),
|
|
21
|
+
* ],
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
interface IamSocialProvider {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
type: "oidc";
|
|
32
|
+
issuer: string;
|
|
33
|
+
clientId: string;
|
|
34
|
+
clientSecret?: string;
|
|
35
|
+
authorization: {
|
|
36
|
+
url: string;
|
|
37
|
+
params: {
|
|
38
|
+
scope: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
token: {
|
|
42
|
+
url: string;
|
|
43
|
+
};
|
|
44
|
+
userinfo: {
|
|
45
|
+
url: string;
|
|
46
|
+
};
|
|
47
|
+
profile: (profile: Record<string, unknown>) => {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
email: string;
|
|
51
|
+
image: string | null;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create a BetterAuth-compatible social provider for IAM.
|
|
56
|
+
*
|
|
57
|
+
* Works with BetterAuth's SSO plugin or generic OAuth integration.
|
|
58
|
+
* Uses standard OIDC endpoints.
|
|
59
|
+
*/
|
|
60
|
+
declare function iamProvider(config: IamConfig & {
|
|
61
|
+
redirectUri?: string;
|
|
62
|
+
}): IamSocialProvider;
|
|
63
|
+
|
|
64
|
+
export { type IamSocialProvider as HanzoIamSocialProvider, type IamSocialProvider, iamProvider as hanzoIamProvider, iamProvider as hanzoIamSocialProvider, iamProvider };
|
package/dist/betterauth.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { IamConfig } from './types.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* BetterAuth SSO provider configuration for IAM.
|
|
3
5
|
*
|
|
@@ -22,8 +24,8 @@
|
|
|
22
24
|
*
|
|
23
25
|
* @packageDocumentation
|
|
24
26
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
|
|
28
|
+
interface IamSocialProvider {
|
|
27
29
|
id: string;
|
|
28
30
|
name: string;
|
|
29
31
|
type: "oidc";
|
|
@@ -55,13 +57,8 @@ export interface IamSocialProvider {
|
|
|
55
57
|
* Works with BetterAuth's SSO plugin or generic OAuth integration.
|
|
56
58
|
* Uses standard OIDC endpoints.
|
|
57
59
|
*/
|
|
58
|
-
|
|
60
|
+
declare function iamProvider(config: IamConfig & {
|
|
59
61
|
redirectUri?: string;
|
|
60
62
|
}): IamSocialProvider;
|
|
61
|
-
|
|
62
|
-
export { iamProvider as hanzoIamProvider };
|
|
63
|
-
/** @deprecated Use iamProvider instead */
|
|
64
|
-
export { iamProvider as hanzoIamSocialProvider };
|
|
65
|
-
/** @deprecated Use IamSocialProvider instead */
|
|
66
|
-
export type { IamSocialProvider as HanzoIamSocialProvider };
|
|
67
|
-
//# sourceMappingURL=betterauth.d.ts.map
|
|
63
|
+
|
|
64
|
+
export { type IamSocialProvider as HanzoIamSocialProvider, type IamSocialProvider, iamProvider as hanzoIamProvider, iamProvider as hanzoIamSocialProvider, iamProvider };
|