@hanzo/iam 0.3.0 → 0.4.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.d.ts.map +1 -1
- package/dist/auth.js +8 -3
- package/dist/auth.js.map +1 -1
- package/dist/betterauth.d.ts +67 -0
- package/dist/betterauth.d.ts.map +1 -0
- package/dist/betterauth.js +64 -0
- package/dist/betterauth.js.map +1 -0
- package/dist/billing.d.ts +9 -103
- package/dist/billing.d.ts.map +1 -1
- package/dist/billing.js +11 -121
- package/dist/billing.js.map +1 -1
- package/dist/browser.d.ts +8 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +58 -15
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/nextauth.d.ts +12 -10
- package/dist/nextauth.d.ts.map +1 -1
- package/dist/nextauth.js +12 -11
- package/dist/nextauth.js.map +1 -1
- package/dist/passport.d.ts +44 -0
- package/dist/passport.d.ts.map +1 -0
- package/dist/passport.js +67 -0
- package/dist/passport.js.map +1 -0
- package/package.json +11 -3
- package/src/auth.ts +10 -3
- package/src/betterauth.ts +91 -0
- package/src/billing.ts +12 -198
- package/src/browser.ts +72 -16
- package/src/index.ts +2 -3
- package/src/nextauth.ts +15 -13
- package/src/passport.ts +97 -0
package/src/billing.ts
CHANGED
|
@@ -1,205 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @hanzo/iam/billing —
|
|
2
|
+
* @hanzo/iam/billing — REMOVED
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* client for convenience when @hanzo/iam is already installed.
|
|
6
|
-
* Both talk to Commerce API — one way to do billing.
|
|
4
|
+
* Billing has moved to @hanzo/commerce (or commerce.js).
|
|
7
5
|
*
|
|
8
|
-
* @example
|
|
9
6
|
* ```ts
|
|
10
|
-
* //
|
|
11
|
-
* import {
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* import { BillingClient } from '@hanzo/iam/billing'
|
|
7
|
+
* // Use this instead:
|
|
8
|
+
* import { Commerce } from '@hanzo/commerce'
|
|
9
|
+
* const commerce = new Commerce({ commerceUrl: '...' })
|
|
10
|
+
* await commerce.getBalance(userId)
|
|
15
11
|
* ```
|
|
12
|
+
*
|
|
13
|
+
* @deprecated This module is no longer functional. Use @hanzo/commerce.
|
|
16
14
|
*/
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
export type CommerceConfig = {
|
|
25
|
-
/** Commerce API base URL (e.g. "https://commerce.hanzo.ai"). */
|
|
26
|
-
commerceUrl: string;
|
|
27
|
-
/** Optional IAM access token for authenticated requests. */
|
|
28
|
-
token?: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Types
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
export type Balance = {
|
|
36
|
-
balance: number;
|
|
37
|
-
holds: number;
|
|
38
|
-
available: number;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type Transaction = {
|
|
42
|
-
id?: string;
|
|
43
|
-
type: "hold" | "hold-removed" | "transfer" | "deposit" | "withdraw";
|
|
44
|
-
currency: string;
|
|
45
|
-
amount: number;
|
|
46
|
-
tags?: string[];
|
|
47
|
-
expiresAt?: string;
|
|
48
|
-
metadata?: Record<string, unknown>;
|
|
49
|
-
createdAt?: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export type Subscription = {
|
|
53
|
-
id?: string;
|
|
54
|
-
planId?: string;
|
|
55
|
-
userId?: string;
|
|
56
|
-
status?: string;
|
|
57
|
-
billingType?: string;
|
|
58
|
-
periodStart?: string;
|
|
59
|
-
periodEnd?: string;
|
|
60
|
-
createdAt?: string;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export type Plan = {
|
|
64
|
-
slug?: string;
|
|
65
|
-
name?: string;
|
|
66
|
-
description?: string;
|
|
67
|
-
price?: number;
|
|
68
|
-
currency?: string;
|
|
69
|
-
interval?: string;
|
|
70
|
-
metadata?: Record<string, unknown>;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export type Payment = {
|
|
74
|
-
id?: string;
|
|
75
|
-
orderId?: string;
|
|
76
|
-
amount?: number;
|
|
77
|
-
currency?: string;
|
|
78
|
-
status?: string;
|
|
79
|
-
captured?: boolean;
|
|
80
|
-
createdAt?: string;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Client
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
export class BillingClient {
|
|
88
|
-
private readonly baseUrl: string;
|
|
89
|
-
private token: string | undefined;
|
|
90
|
-
|
|
91
|
-
constructor(config: CommerceConfig) {
|
|
92
|
-
this.baseUrl = config.commerceUrl.replace(/\/+$/, "");
|
|
93
|
-
this.token = config.token;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
setToken(token: string) {
|
|
97
|
-
this.token = token;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private async request<T>(
|
|
101
|
-
path: string,
|
|
102
|
-
opts?: { method?: string; body?: unknown; token?: string; params?: Record<string, string> },
|
|
103
|
-
): Promise<T> {
|
|
104
|
-
const url = new URL(path, this.baseUrl);
|
|
105
|
-
if (opts?.params) {
|
|
106
|
-
for (const [k, v] of Object.entries(opts.params)) url.searchParams.set(k, v);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const controller = new AbortController();
|
|
110
|
-
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
111
|
-
|
|
112
|
-
const headers: Record<string, string> = { Accept: "application/json" };
|
|
113
|
-
const authToken = opts?.token ?? this.token;
|
|
114
|
-
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
|
115
|
-
if (opts?.body) headers["Content-Type"] = "application/json";
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
const res = await fetch(url.toString(), {
|
|
119
|
-
method: opts?.method ?? "GET",
|
|
120
|
-
headers,
|
|
121
|
-
body: opts?.body ? JSON.stringify(opts.body) : undefined,
|
|
122
|
-
signal: controller.signal,
|
|
123
|
-
});
|
|
124
|
-
if (!res.ok) {
|
|
125
|
-
const text = await res.text().catch(() => "");
|
|
126
|
-
throw new CommerceApiError(res.status, `${res.statusText}: ${text}`.trim());
|
|
127
|
-
}
|
|
128
|
-
return (await res.json()) as T;
|
|
129
|
-
} finally {
|
|
130
|
-
clearTimeout(timer);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async getBalance(user: string, currency = "usd", token?: string): Promise<Balance> {
|
|
135
|
-
return this.request("/api/v1/billing/balance", { params: { user, currency }, token });
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async getAllBalances(user: string, token?: string): Promise<Record<string, Balance>> {
|
|
139
|
-
return this.request("/api/v1/billing/balance/all", { params: { user }, token });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async addUsageRecord(record: { user: string; currency?: string; amount: number; model?: string; provider?: string; tokens?: number }, token?: string): Promise<Transaction> {
|
|
143
|
-
return this.request("/api/v1/billing/usage", { method: "POST", body: record, token });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async getUsageRecords(user: string, currency = "usd", token?: string): Promise<Transaction[]> {
|
|
147
|
-
return this.request("/api/v1/billing/usage", { params: { user, currency }, token });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async addDeposit(params: { user: string; currency?: string; amount: number; notes?: string; tags?: string[]; expiresIn?: string }, token?: string): Promise<Transaction> {
|
|
151
|
-
return this.request("/api/v1/billing/deposit", { method: "POST", body: params, token });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async grantStarterCredit(user: string, token?: string): Promise<Transaction> {
|
|
155
|
-
return this.request("/api/v1/billing/credit", { method: "POST", body: { user }, token });
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async subscribe(params: { planId: string; userId: string }, token?: string): Promise<Subscription> {
|
|
159
|
-
return this.request("/api/v1/subscribe", { method: "POST", body: params, token });
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async getSubscription(id: string, token?: string): Promise<Subscription | null> {
|
|
163
|
-
try { return await this.request(`/api/v1/subscribe/${id}`, { token }); } catch { return null; }
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async cancelSubscription(id: string, token?: string): Promise<void> {
|
|
167
|
-
await this.request(`/api/v1/subscribe/${id}`, { method: "DELETE", token });
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async getPlans(token?: string): Promise<Plan[]> {
|
|
171
|
-
return this.request("/api/v1/plan", { token });
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async getPlan(id: string, token?: string): Promise<Plan | null> {
|
|
175
|
-
try { return await this.request(`/api/v1/plan/${id}`, { token }); } catch { return null; }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async authorize(orderId: string, token?: string): Promise<Payment> {
|
|
179
|
-
return this.request(`/api/v1/authorize/${orderId}`, { method: "POST", token });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async capture(orderId: string, token?: string): Promise<Payment> {
|
|
183
|
-
return this.request(`/api/v1/capture/${orderId}`, { method: "POST", token });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async charge(orderId: string, token?: string): Promise<Payment> {
|
|
187
|
-
return this.request(`/api/v1/charge/${orderId}`, { method: "POST", token });
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async refund(paymentId: string, token?: string): Promise<Payment> {
|
|
191
|
-
return this.request(`/api/v1/refund/${paymentId}`, { method: "POST", token });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export class CommerceApiError extends Error {
|
|
196
|
-
readonly status: number;
|
|
197
|
-
constructor(status: number, message: string) {
|
|
198
|
-
super(message);
|
|
199
|
-
this.name = "CommerceApiError";
|
|
200
|
-
this.status = status;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Backwards-compatible alias
|
|
205
|
-
export { BillingClient as IamBillingClient };
|
|
16
|
+
throw new Error(
|
|
17
|
+
'@hanzo/iam/billing has been removed. Use @hanzo/commerce or commerce.js instead. ' +
|
|
18
|
+
'See: https://docs.hanzo.ai/services/commerce/sdk'
|
|
19
|
+
)
|
package/src/browser.ts
CHANGED
|
@@ -33,6 +33,14 @@ export type BrowserIamConfig = IamConfig & {
|
|
|
33
33
|
scope?: string;
|
|
34
34
|
/** Storage to use for tokens (default: sessionStorage). */
|
|
35
35
|
storage?: Storage;
|
|
36
|
+
/**
|
|
37
|
+
* Proxy base URL for token exchange and userinfo requests.
|
|
38
|
+
* When set, token exchange POSTs go to `${proxyBaseUrl}/auth/token`
|
|
39
|
+
* and userinfo GETs go to `${proxyBaseUrl}/auth/userinfo` instead of
|
|
40
|
+
* directly to the IAM server. This avoids CORS issues when the IAM
|
|
41
|
+
* server doesn't send Access-Control-Allow-Origin headers.
|
|
42
|
+
*/
|
|
43
|
+
proxyBaseUrl?: string;
|
|
36
44
|
};
|
|
37
45
|
|
|
38
46
|
export class BrowserIamSdk {
|
|
@@ -53,13 +61,32 @@ export class BrowserIamSdk {
|
|
|
53
61
|
if (this.discoveryCache) return this.discoveryCache;
|
|
54
62
|
|
|
55
63
|
const baseUrl = this.config.serverUrl.replace(/\/+$/, "");
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
|
|
65
|
+
// Try fetching the OIDC discovery document. If it fails (e.g. due to
|
|
66
|
+
// CORS when the IAM server doesn't send Access-Control-Allow-Origin),
|
|
67
|
+
// construct a fallback from well-known Casdoor/Hanzo IAM endpoint paths.
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(`${baseUrl}/.well-known/openid-configuration`, {
|
|
70
|
+
headers: { Accept: "application/json" },
|
|
71
|
+
});
|
|
72
|
+
if (res.ok) {
|
|
73
|
+
this.discoveryCache = (await res.json()) as OidcDiscovery;
|
|
74
|
+
return this.discoveryCache;
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// CORS or network error — fall through to constructed discovery
|
|
61
78
|
}
|
|
62
|
-
|
|
79
|
+
|
|
80
|
+
this.discoveryCache = {
|
|
81
|
+
issuer: baseUrl,
|
|
82
|
+
authorization_endpoint: `${baseUrl}/login/oauth/authorize`,
|
|
83
|
+
token_endpoint: `${baseUrl}/api/login/oauth/access_token`,
|
|
84
|
+
userinfo_endpoint: `${baseUrl}/api/userinfo`,
|
|
85
|
+
jwks_uri: `${baseUrl}/.well-known/jwks`,
|
|
86
|
+
response_types_supported: ["code", "token", "id_token"],
|
|
87
|
+
grant_types_supported: ["authorization_code", "implicit", "refresh_token"],
|
|
88
|
+
scopes_supported: ["openid", "email", "profile"],
|
|
89
|
+
};
|
|
63
90
|
return this.discoveryCache;
|
|
64
91
|
}
|
|
65
92
|
|
|
@@ -112,8 +139,6 @@ export class BrowserIamSdk {
|
|
|
112
139
|
*/
|
|
113
140
|
async handleCallback(callbackUrl?: string): Promise<TokenResponse> {
|
|
114
141
|
const url = new URL(callbackUrl ?? window.location.href);
|
|
115
|
-
const code = url.searchParams.get("code");
|
|
116
|
-
const state = url.searchParams.get("state");
|
|
117
142
|
const error = url.searchParams.get("error");
|
|
118
143
|
|
|
119
144
|
if (error) {
|
|
@@ -121,15 +146,34 @@ export class BrowserIamSdk {
|
|
|
121
146
|
throw new Error(`OAuth error: ${desc}`);
|
|
122
147
|
}
|
|
123
148
|
|
|
124
|
-
|
|
125
|
-
throw new Error("Missing authorization code in callback URL");
|
|
126
|
-
}
|
|
127
|
-
|
|
149
|
+
const state = url.searchParams.get("state");
|
|
128
150
|
const savedState = this.storage.getItem(KEY_STATE);
|
|
129
|
-
if (
|
|
151
|
+
if (savedState && state !== savedState) {
|
|
130
152
|
throw new Error("OAuth state mismatch — possible CSRF attack");
|
|
131
153
|
}
|
|
132
154
|
|
|
155
|
+
// Implicit flow: access_token returned directly in URL
|
|
156
|
+
const accessToken = url.searchParams.get("access_token");
|
|
157
|
+
if (accessToken) {
|
|
158
|
+
this.storage.removeItem(KEY_STATE);
|
|
159
|
+
this.storage.removeItem(KEY_CODE_VERIFIER);
|
|
160
|
+
|
|
161
|
+
const tokens: TokenResponse = {
|
|
162
|
+
access_token: accessToken,
|
|
163
|
+
token_type: "Bearer",
|
|
164
|
+
refresh_token: url.searchParams.get("refresh_token") ?? undefined,
|
|
165
|
+
expires_in: 7200,
|
|
166
|
+
};
|
|
167
|
+
this.storeTokens(tokens);
|
|
168
|
+
return tokens;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Authorization code flow: exchange code for tokens via PKCE
|
|
172
|
+
const code = url.searchParams.get("code");
|
|
173
|
+
if (!code) {
|
|
174
|
+
throw new Error("Missing authorization code in callback URL");
|
|
175
|
+
}
|
|
176
|
+
|
|
133
177
|
const codeVerifier = this.storage.getItem(KEY_CODE_VERIFIER);
|
|
134
178
|
if (!codeVerifier) {
|
|
135
179
|
throw new Error("Missing PKCE code verifier — was signinRedirect() called?");
|
|
@@ -148,7 +192,12 @@ export class BrowserIamSdk {
|
|
|
148
192
|
code_verifier: codeVerifier,
|
|
149
193
|
});
|
|
150
194
|
|
|
151
|
-
|
|
195
|
+
// Use proxy URL when configured to avoid CORS on the token endpoint.
|
|
196
|
+
const tokenUrl = this.config.proxyBaseUrl
|
|
197
|
+
? `${this.config.proxyBaseUrl.replace(/\/+$/, "")}/auth/token`
|
|
198
|
+
: discovery.token_endpoint;
|
|
199
|
+
|
|
200
|
+
const res = await fetch(tokenUrl, {
|
|
152
201
|
method: "POST",
|
|
153
202
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
154
203
|
body: body.toString(),
|
|
@@ -182,7 +231,11 @@ export class BrowserIamSdk {
|
|
|
182
231
|
refresh_token: refreshToken,
|
|
183
232
|
});
|
|
184
233
|
|
|
185
|
-
const
|
|
234
|
+
const tokenUrl = this.config.proxyBaseUrl
|
|
235
|
+
? `${this.config.proxyBaseUrl.replace(/\/+$/, "")}/auth/token`
|
|
236
|
+
: discovery.token_endpoint;
|
|
237
|
+
|
|
238
|
+
const res = await fetch(tokenUrl, {
|
|
186
239
|
method: "POST",
|
|
187
240
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
188
241
|
body: body.toString(),
|
|
@@ -417,7 +470,10 @@ export class BrowserIamSdk {
|
|
|
417
470
|
throw new Error("No valid access token — user must log in");
|
|
418
471
|
}
|
|
419
472
|
const discovery = await this.getDiscovery();
|
|
420
|
-
const
|
|
473
|
+
const userinfoUrl = this.config.proxyBaseUrl
|
|
474
|
+
? `${this.config.proxyBaseUrl.replace(/\/+$/, "")}/auth/userinfo`
|
|
475
|
+
: discovery.userinfo_endpoint;
|
|
476
|
+
const res = await fetch(userinfoUrl, {
|
|
421
477
|
headers: { Authorization: `Bearer ${token}` },
|
|
422
478
|
});
|
|
423
479
|
if (!res.ok) {
|
package/src/index.ts
CHANGED
|
@@ -22,9 +22,8 @@
|
|
|
22
22
|
// Core client (auth, users, orgs, projects → IAM)
|
|
23
23
|
export { IamClient, IamApiError } from "./client.js";
|
|
24
24
|
|
|
25
|
-
// Billing
|
|
26
|
-
//
|
|
27
|
-
export { BillingClient, IamBillingClient, CommerceApiError } from "./billing.js";
|
|
25
|
+
// Billing has moved to @hanzo/commerce. Import Commerce from "@hanzo/commerce" instead.
|
|
26
|
+
// See: https://docs.hanzo.ai/services/commerce/sdk
|
|
28
27
|
|
|
29
28
|
// JWT validation
|
|
30
29
|
export { validateToken, clearJwksCache } from "./auth.js";
|
package/src/nextauth.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* NextAuth.js provider for
|
|
2
|
+
* NextAuth.js / Auth.js provider for IAM (OIDC-based).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* so all Next.js apps can share one
|
|
4
|
+
* Provides a canonical NextAuth/Auth.js provider configuration
|
|
5
|
+
* so all Next.js apps can share one implementation.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
9
9
|
* // next-auth config
|
|
10
|
-
* import {
|
|
10
|
+
* import { IamProvider } from "@hanzo/iam/nextauth";
|
|
11
11
|
*
|
|
12
12
|
* export default NextAuth({
|
|
13
13
|
* providers: [
|
|
14
|
-
*
|
|
14
|
+
* IamProvider({
|
|
15
15
|
* serverUrl: process.env.IAM_SERVER_URL!,
|
|
16
16
|
* clientId: process.env.IAM_CLIENT_ID!,
|
|
17
17
|
* clientSecret: process.env.IAM_CLIENT_SECRET!,
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @packageDocumentation
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
interface
|
|
26
|
+
export interface IamProfile extends Record<string, unknown> {
|
|
27
27
|
sub: string;
|
|
28
28
|
name: string;
|
|
29
29
|
email: string;
|
|
@@ -35,7 +35,7 @@ interface HanzoIamProfile extends Record<string, unknown> {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* NextAuth.js / Auth.js compatible OAuth provider for
|
|
38
|
+
* NextAuth.js / Auth.js compatible OAuth provider for IAM.
|
|
39
39
|
*
|
|
40
40
|
* Uses standard OIDC well-known endpoint for automatic configuration.
|
|
41
41
|
* JWT id_token validation (issuer, audience, signature) is handled by
|
|
@@ -43,7 +43,7 @@ interface HanzoIamProfile extends Record<string, unknown> {
|
|
|
43
43
|
*
|
|
44
44
|
* Pass `checks: ["state", "pkce"]` in options for PKCE alignment.
|
|
45
45
|
*/
|
|
46
|
-
export function
|
|
46
|
+
export function IamProvider<P extends IamProfile>(
|
|
47
47
|
options: {
|
|
48
48
|
serverUrl: string;
|
|
49
49
|
clientId: string;
|
|
@@ -59,8 +59,8 @@ export function HanzoIamProvider<P extends HanzoIamProfile>(
|
|
|
59
59
|
const checks = options.checks ?? ["state"];
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
|
-
id: "
|
|
63
|
-
name: "
|
|
62
|
+
id: "iam",
|
|
63
|
+
name: "IAM",
|
|
64
64
|
type: "oauth",
|
|
65
65
|
wellKnown: `${issuer}/.well-known/openid-configuration`,
|
|
66
66
|
idToken: true,
|
|
@@ -88,6 +88,8 @@ export function HanzoIamProvider<P extends HanzoIamProfile>(
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
export
|
|
91
|
+
// Backwards-compatible aliases
|
|
92
|
+
/** @deprecated Use IamProvider instead */
|
|
93
|
+
export { IamProvider as HanzoIamProvider };
|
|
94
|
+
/** @deprecated Use IamProfile instead */
|
|
95
|
+
export type { IamProfile as HanzoIamProfile };
|
package/src/passport.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passport.js OAuth2 strategy factory for Hanzo IAM.
|
|
3
|
+
*
|
|
4
|
+
* Creates a pre-configured passport-oauth2 strategy that authenticates
|
|
5
|
+
* against hanzo.id with PKCE and fetches user info on callback.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import passport from "passport";
|
|
10
|
+
* import { createIamPassportStrategy } from "@hanzo/iam/passport";
|
|
11
|
+
*
|
|
12
|
+
* passport.use("iam", createIamPassportStrategy({
|
|
13
|
+
* serverUrl: "https://hanzo.id",
|
|
14
|
+
* clientId: "hanzo-kms-client-id",
|
|
15
|
+
* clientSecret: process.env.IAM_CLIENT_SECRET!,
|
|
16
|
+
* callbackUrl: "https://kms.hanzo.ai/api/v1/sso/oidc/callback",
|
|
17
|
+
* }));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { IamConfig } from "./types.js";
|
|
24
|
+
|
|
25
|
+
export interface IamPassportConfig extends IamConfig {
|
|
26
|
+
/** Full callback URL for OAuth2 redirect. */
|
|
27
|
+
callbackUrl: string;
|
|
28
|
+
/** OAuth2 scopes. Default: "openid profile email". */
|
|
29
|
+
scope?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IamPassportUser {
|
|
33
|
+
accessToken: string;
|
|
34
|
+
refreshToken?: string;
|
|
35
|
+
userinfo: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a Passport OAuth2 strategy for Hanzo IAM.
|
|
40
|
+
*
|
|
41
|
+
* Requires `passport-oauth2` as a peer dependency.
|
|
42
|
+
* Returns an OAuth2Strategy instance ready to pass to `passport.use()`.
|
|
43
|
+
*
|
|
44
|
+
* The verify callback fetches userinfo from the IAM server and passes
|
|
45
|
+
* `{ accessToken, refreshToken, userinfo }` as the user object.
|
|
46
|
+
*/
|
|
47
|
+
export function createIamPassportStrategy(
|
|
48
|
+
config: IamPassportConfig,
|
|
49
|
+
): unknown {
|
|
50
|
+
// Dynamic import to keep passport-oauth2 as optional peer dep.
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
|
+
const { Strategy: OAuth2Strategy } = require("passport-oauth2") as {
|
|
53
|
+
Strategy: new (
|
|
54
|
+
options: Record<string, unknown>,
|
|
55
|
+
verify: (...args: unknown[]) => void,
|
|
56
|
+
) => unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const baseUrl = config.serverUrl.replace(/\/+$/, "");
|
|
60
|
+
|
|
61
|
+
const verify = async (
|
|
62
|
+
...args: unknown[]
|
|
63
|
+
): Promise<void> => {
|
|
64
|
+
// passReqToCallback=true: (req, accessToken, refreshToken, profile, done)
|
|
65
|
+
const accessToken = args[1] as string;
|
|
66
|
+
const refreshToken = args[2] as string | undefined;
|
|
67
|
+
const done = args[4] as (err: Error | null, user?: IamPassportUser) => void;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(`${baseUrl}/api/userinfo`, {
|
|
71
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
return done(new Error(`IAM userinfo failed: ${res.status}`));
|
|
75
|
+
}
|
|
76
|
+
const userinfo = (await res.json()) as Record<string, unknown>;
|
|
77
|
+
done(null, { accessToken, refreshToken, userinfo });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return new OAuth2Strategy(
|
|
84
|
+
{
|
|
85
|
+
authorizationURL: `${baseUrl}/login/oauth/authorize`,
|
|
86
|
+
tokenURL: `${baseUrl}/api/login/oauth/access_token`,
|
|
87
|
+
clientID: config.clientId,
|
|
88
|
+
clientSecret: config.clientSecret ?? "",
|
|
89
|
+
callbackURL: config.callbackUrl,
|
|
90
|
+
scope: config.scope ?? "openid profile email",
|
|
91
|
+
state: true,
|
|
92
|
+
pkce: true,
|
|
93
|
+
passReqToCallback: true,
|
|
94
|
+
},
|
|
95
|
+
verify,
|
|
96
|
+
);
|
|
97
|
+
}
|