@hanzo/iam 0.1.0 → 0.3.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/billing.d.ts +104 -35
- package/dist/billing.d.ts.map +1 -1
- package/dist/billing.js +89 -115
- package/dist/billing.js.map +1 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -0
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +9 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/nextauth.d.ts +56 -0
- package/dist/nextauth.d.ts.map +1 -0
- package/dist/nextauth.js +67 -0
- package/dist/nextauth.js.map +1 -0
- package/dist/react.d.ts +42 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +100 -0
- package/dist/react.js.map +1 -1
- package/dist/types.d.ts +52 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/billing.ts +164 -197
- package/src/client.ts +36 -0
- package/src/index.ts +22 -10
- package/src/nextauth.ts +93 -0
- package/src/react.ts +187 -1
- package/src/types.ts +62 -10
package/src/billing.ts
CHANGED
|
@@ -1,72 +1,118 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Billing client for Hanzo
|
|
2
|
+
* @hanzo/iam/billing — Billing client for Hanzo Commerce API.
|
|
3
|
+
*
|
|
4
|
+
* Canonical billing lives in commerce.js/billing. This provides the same
|
|
5
|
+
* client for convenience when @hanzo/iam is already installed.
|
|
6
|
+
* Both talk to Commerce API — one way to do billing.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* // Preferred:
|
|
11
|
+
* import { BillingClient } from 'commerce.js/billing'
|
|
12
|
+
*
|
|
13
|
+
* // Also works:
|
|
14
|
+
* import { BillingClient } from '@hanzo/iam/billing'
|
|
15
|
+
* ```
|
|
3
16
|
*/
|
|
4
17
|
|
|
5
|
-
import type {
|
|
6
|
-
IamConfig,
|
|
7
|
-
IamSubscription,
|
|
8
|
-
IamPlan,
|
|
9
|
-
IamPricing,
|
|
10
|
-
IamPayment,
|
|
11
|
-
IamOrder,
|
|
12
|
-
IamApiResponse,
|
|
13
|
-
} from "./types.js";
|
|
14
|
-
|
|
15
18
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Config
|
|
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 {
|
|
18
88
|
private readonly baseUrl: string;
|
|
19
|
-
private
|
|
20
|
-
private readonly clientSecret: string | undefined;
|
|
21
|
-
private readonly orgName: string | undefined;
|
|
89
|
+
private token: string | undefined;
|
|
22
90
|
|
|
23
|
-
constructor(config:
|
|
24
|
-
this.baseUrl = config.
|
|
25
|
-
this.
|
|
26
|
-
this.clientSecret = config.clientSecret;
|
|
27
|
-
this.orgName = config.orgName;
|
|
91
|
+
constructor(config: CommerceConfig) {
|
|
92
|
+
this.baseUrl = config.commerceUrl.replace(/\/+$/, "");
|
|
93
|
+
this.token = config.token;
|
|
28
94
|
}
|
|
29
95
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
96
|
+
setToken(token: string) {
|
|
97
|
+
this.token = token;
|
|
98
|
+
}
|
|
33
99
|
|
|
34
100
|
private async request<T>(
|
|
35
101
|
path: string,
|
|
36
|
-
opts?: {
|
|
37
|
-
method?: string;
|
|
38
|
-
body?: unknown;
|
|
39
|
-
token?: string;
|
|
40
|
-
params?: Record<string, string>;
|
|
41
|
-
},
|
|
102
|
+
opts?: { method?: string; body?: unknown; token?: string; params?: Record<string, string> },
|
|
42
103
|
): Promise<T> {
|
|
43
104
|
const url = new URL(path, this.baseUrl);
|
|
44
105
|
if (opts?.params) {
|
|
45
|
-
for (const [k, v] of Object.entries(opts.params))
|
|
46
|
-
url.searchParams.set(k, v);
|
|
47
|
-
}
|
|
106
|
+
for (const [k, v] of Object.entries(opts.params)) url.searchParams.set(k, v);
|
|
48
107
|
}
|
|
49
108
|
|
|
50
109
|
const controller = new AbortController();
|
|
51
110
|
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
52
111
|
|
|
53
|
-
const headers: Record<string, string> = {
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
if (opts?.
|
|
57
|
-
headers.Authorization = `Bearer ${opts.token}`;
|
|
58
|
-
}
|
|
59
|
-
if (opts?.body) {
|
|
60
|
-
headers["Content-Type"] = "application/json";
|
|
61
|
-
}
|
|
62
|
-
if (this.clientSecret && !opts?.token) {
|
|
63
|
-
const credentials = `${this.clientId}:${this.clientSecret}`;
|
|
64
|
-
const basic =
|
|
65
|
-
typeof Buffer !== "undefined"
|
|
66
|
-
? Buffer.from(credentials).toString("base64")
|
|
67
|
-
: btoa(credentials);
|
|
68
|
-
headers.Authorization = `Basic ${basic}`;
|
|
69
|
-
}
|
|
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";
|
|
70
116
|
|
|
71
117
|
try {
|
|
72
118
|
const res = await fetch(url.toString(), {
|
|
@@ -75,164 +121,85 @@ export class IamBillingClient {
|
|
|
75
121
|
body: opts?.body ? JSON.stringify(opts.body) : undefined,
|
|
76
122
|
signal: controller.signal,
|
|
77
123
|
});
|
|
78
|
-
|
|
79
124
|
if (!res.ok) {
|
|
80
125
|
const text = await res.text().catch(() => "");
|
|
81
|
-
throw new
|
|
126
|
+
throw new CommerceApiError(res.status, `${res.statusText}: ${text}`.trim());
|
|
82
127
|
}
|
|
83
|
-
|
|
84
128
|
return (await res.json()) as T;
|
|
85
129
|
} finally {
|
|
86
130
|
clearTimeout(timer);
|
|
87
131
|
}
|
|
88
132
|
}
|
|
89
133
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
/** Get all subscriptions for an owner. */
|
|
95
|
-
async getSubscriptions(token?: string): Promise<IamSubscription[]> {
|
|
96
|
-
const owner = this.orgName ?? "admin";
|
|
97
|
-
const resp = await this.request<IamApiResponse<IamSubscription[]>>(
|
|
98
|
-
"/api/get-subscriptions",
|
|
99
|
-
{ params: { owner }, token },
|
|
100
|
-
);
|
|
101
|
-
return resp.data ?? [];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Get a specific subscription by ID ("owner/name" format). */
|
|
105
|
-
async getSubscription(id: string, token?: string): Promise<IamSubscription | null> {
|
|
106
|
-
const resp = await this.request<IamApiResponse<IamSubscription>>(
|
|
107
|
-
"/api/get-subscription",
|
|
108
|
-
{ params: { id }, token },
|
|
109
|
-
);
|
|
110
|
-
return resp.data ?? null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Get the subscription for a specific user. */
|
|
114
|
-
async getUserSubscription(userId: string, token?: string): Promise<IamSubscription | null> {
|
|
115
|
-
const subs = await this.getSubscriptions(token);
|
|
116
|
-
return subs.find((s) => s.user === userId) ?? null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// -----------------------------------------------------------------------
|
|
120
|
-
// Plans
|
|
121
|
-
// -----------------------------------------------------------------------
|
|
122
|
-
|
|
123
|
-
/** Get all plans for an owner. */
|
|
124
|
-
async getPlans(token?: string): Promise<IamPlan[]> {
|
|
125
|
-
const owner = this.orgName ?? "admin";
|
|
126
|
-
const resp = await this.request<IamApiResponse<IamPlan[]>>(
|
|
127
|
-
"/api/get-plans",
|
|
128
|
-
{ params: { owner }, token },
|
|
129
|
-
);
|
|
130
|
-
return resp.data ?? [];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Get a specific plan by ID. */
|
|
134
|
-
async getPlan(id: string, token?: string): Promise<IamPlan | null> {
|
|
135
|
-
const resp = await this.request<IamApiResponse<IamPlan>>(
|
|
136
|
-
"/api/get-plan",
|
|
137
|
-
{ params: { id }, token },
|
|
138
|
-
);
|
|
139
|
-
return resp.data ?? null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// -----------------------------------------------------------------------
|
|
143
|
-
// Pricing
|
|
144
|
-
// -----------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
/** Get all pricing configurations for an owner. */
|
|
147
|
-
async getPricings(token?: string): Promise<IamPricing[]> {
|
|
148
|
-
const owner = this.orgName ?? "admin";
|
|
149
|
-
const resp = await this.request<IamApiResponse<IamPricing[]>>(
|
|
150
|
-
"/api/get-pricings",
|
|
151
|
-
{ params: { owner }, token },
|
|
152
|
-
);
|
|
153
|
-
return resp.data ?? [];
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Get a specific pricing by ID. */
|
|
157
|
-
async getPricing(id: string, token?: string): Promise<IamPricing | null> {
|
|
158
|
-
const resp = await this.request<IamApiResponse<IamPricing>>(
|
|
159
|
-
"/api/get-pricing",
|
|
160
|
-
{ params: { id }, token },
|
|
161
|
-
);
|
|
162
|
-
return resp.data ?? null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// -----------------------------------------------------------------------
|
|
166
|
-
// Payments
|
|
167
|
-
// -----------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
/** Get all payments for an owner. */
|
|
170
|
-
async getPayments(token?: string): Promise<IamPayment[]> {
|
|
171
|
-
const owner = this.orgName ?? "admin";
|
|
172
|
-
const resp = await this.request<IamApiResponse<IamPayment[]>>(
|
|
173
|
-
"/api/get-payments",
|
|
174
|
-
{ params: { owner }, token },
|
|
175
|
-
);
|
|
176
|
-
return resp.data ?? [];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Get a specific payment by ID. */
|
|
180
|
-
async getPayment(id: string, token?: string): Promise<IamPayment | null> {
|
|
181
|
-
const resp = await this.request<IamApiResponse<IamPayment>>(
|
|
182
|
-
"/api/get-payment",
|
|
183
|
-
{ params: { id }, token },
|
|
184
|
-
);
|
|
185
|
-
return resp.data ?? null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// -----------------------------------------------------------------------
|
|
189
|
-
// Orders (if supported by IAM)
|
|
190
|
-
// -----------------------------------------------------------------------
|
|
191
|
-
|
|
192
|
-
/** Get all orders for an owner. */
|
|
193
|
-
async getOrders(token?: string): Promise<IamOrder[]> {
|
|
194
|
-
const owner = this.orgName ?? "admin";
|
|
195
|
-
const resp = await this.request<IamApiResponse<IamOrder[]>>(
|
|
196
|
-
"/api/get-orders",
|
|
197
|
-
{ params: { owner }, token },
|
|
198
|
-
);
|
|
199
|
-
return resp.data ?? [];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Get a specific order by ID. */
|
|
203
|
-
async getOrder(id: string, token?: string): Promise<IamOrder | null> {
|
|
204
|
-
const resp = await this.request<IamApiResponse<IamOrder>>(
|
|
205
|
-
"/api/get-order",
|
|
206
|
-
{ params: { id }, token },
|
|
207
|
-
);
|
|
208
|
-
return resp.data ?? null;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// -----------------------------------------------------------------------
|
|
212
|
-
// Convenience: check subscription status for an org
|
|
213
|
-
// -----------------------------------------------------------------------
|
|
214
|
-
|
|
215
|
-
/** Check if an org has an active subscription. */
|
|
216
|
-
async isSubscriptionActive(orgName: string, token?: string): Promise<{
|
|
217
|
-
active: boolean;
|
|
218
|
-
subscription: IamSubscription | null;
|
|
219
|
-
plan: IamPlan | null;
|
|
220
|
-
}> {
|
|
221
|
-
const subs = await this.getSubscriptions(token);
|
|
222
|
-
// Find subscription matching the org
|
|
223
|
-
const sub = subs.find(
|
|
224
|
-
(s) => s.owner === orgName && (s.state === "Active" || s.state === "active"),
|
|
225
|
-
) ?? null;
|
|
226
|
-
|
|
227
|
-
if (!sub) {
|
|
228
|
-
return { active: false, subscription: null, plan: null };
|
|
229
|
-
}
|
|
134
|
+
async getBalance(user: string, currency = "usd", token?: string): Promise<Balance> {
|
|
135
|
+
return this.request("/api/v1/billing/balance", { params: { user, currency }, token });
|
|
136
|
+
}
|
|
230
137
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
}
|
|
235
157
|
|
|
236
|
-
|
|
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 });
|
|
237
192
|
}
|
|
238
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 };
|
package/src/client.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
IamApiResponse,
|
|
8
8
|
IamUser,
|
|
9
9
|
IamOrganization,
|
|
10
|
+
IamProject,
|
|
10
11
|
OidcDiscovery,
|
|
11
12
|
TokenResponse,
|
|
12
13
|
} from "./types.js";
|
|
@@ -288,6 +289,41 @@ export class IamClient {
|
|
|
288
289
|
return org ? [org] : [];
|
|
289
290
|
}
|
|
290
291
|
|
|
292
|
+
// -----------------------------------------------------------------------
|
|
293
|
+
// Project
|
|
294
|
+
// -----------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/** List projects (for the configured owner). */
|
|
297
|
+
async getProjects(token?: string): Promise<IamProject[]> {
|
|
298
|
+
const owner = this.orgName ?? "admin";
|
|
299
|
+
const resp = await this.request<IamApiResponse<IamProject[]>>(
|
|
300
|
+
"/api/get-projects",
|
|
301
|
+
{ params: { owner }, token },
|
|
302
|
+
);
|
|
303
|
+
return resp.data ?? [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Get a specific project by ID ("owner/name" format). */
|
|
307
|
+
async getProject(id: string, token?: string): Promise<IamProject | null> {
|
|
308
|
+
const resp = await this.request<IamApiResponse<IamProject>>(
|
|
309
|
+
"/api/get-project",
|
|
310
|
+
{ params: { id }, token },
|
|
311
|
+
);
|
|
312
|
+
return resp.data ?? null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Get all projects for an organization. */
|
|
316
|
+
async getOrganizationProjects(
|
|
317
|
+
organization: string,
|
|
318
|
+
token?: string,
|
|
319
|
+
): Promise<IamProject[]> {
|
|
320
|
+
const resp = await this.request<IamApiResponse<IamProject[]>>(
|
|
321
|
+
"/api/get-organization-projects",
|
|
322
|
+
{ params: { organization }, token },
|
|
323
|
+
);
|
|
324
|
+
return resp.data ?? [];
|
|
325
|
+
}
|
|
326
|
+
|
|
291
327
|
// -----------------------------------------------------------------------
|
|
292
328
|
// Raw request (for extending)
|
|
293
329
|
// -----------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @hanzo/iam — TypeScript SDK for Hanzo IAM (
|
|
2
|
+
* @hanzo/iam — TypeScript SDK for Hanzo IAM (identity & access management).
|
|
3
|
+
*
|
|
4
|
+
* Handles: auth (OIDC, JWT, PKCE), users, organizations, projects.
|
|
5
|
+
* Billing is backed by Commerce — the BillingClient talks to Commerce API.
|
|
3
6
|
*
|
|
4
7
|
* @example
|
|
5
8
|
* ```ts
|
|
6
|
-
* import { IamClient,
|
|
9
|
+
* import { IamClient, BillingClient, validateToken } from "@hanzo/iam";
|
|
7
10
|
*
|
|
8
11
|
* const client = new IamClient({
|
|
9
12
|
* serverUrl: "https://iam.hanzo.ai",
|
|
10
13
|
* clientId: "my-app",
|
|
11
14
|
* });
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* serverUrl: "https://iam.hanzo.ai",
|
|
16
|
-
* clientId: "my-app",
|
|
16
|
+
* const billing = new BillingClient({
|
|
17
|
+
* commerceUrl: "https://commerce.hanzo.ai",
|
|
17
18
|
* });
|
|
18
19
|
* ```
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
|
-
// Core client
|
|
22
|
+
// Core client (auth, users, orgs, projects → IAM)
|
|
22
23
|
export { IamClient, IamApiError } from "./client.js";
|
|
23
24
|
|
|
25
|
+
// Billing client (subscriptions, plans, payments, usage → Commerce API)
|
|
26
|
+
// Canonical source: commerce.js/billing. Re-exported here for convenience.
|
|
27
|
+
export { BillingClient, IamBillingClient, CommerceApiError } from "./billing.js";
|
|
28
|
+
|
|
24
29
|
// JWT validation
|
|
25
30
|
export { validateToken, clearJwksCache } from "./auth.js";
|
|
26
31
|
|
|
27
|
-
// Billing client
|
|
28
|
-
export { IamBillingClient } from "./billing.js";
|
|
29
|
-
|
|
30
32
|
// Browser PKCE auth (re-exported from separate entry point too)
|
|
31
33
|
export { BrowserIamSdk, type BrowserIamConfig } from "./browser.js";
|
|
32
34
|
export { generatePkceChallenge, generateState } from "./pkce.js";
|
|
@@ -42,11 +44,21 @@ export type {
|
|
|
42
44
|
IamJwtClaims,
|
|
43
45
|
IamUser,
|
|
44
46
|
IamOrganization,
|
|
47
|
+
IamProject,
|
|
48
|
+
Subscription,
|
|
49
|
+
Plan,
|
|
50
|
+
Pricing,
|
|
51
|
+
Payment,
|
|
52
|
+
Order,
|
|
53
|
+
UsageRecord,
|
|
54
|
+
UsageSummary,
|
|
45
55
|
IamSubscription,
|
|
46
56
|
IamPlan,
|
|
47
57
|
IamPricing,
|
|
48
58
|
IamPayment,
|
|
49
59
|
IamOrder,
|
|
60
|
+
IamUsageRecord,
|
|
61
|
+
IamUsageSummary,
|
|
50
62
|
IamAuthResult,
|
|
51
63
|
IamApiResponse,
|
|
52
64
|
} from "./types.js";
|
package/src/nextauth.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NextAuth.js provider for Hanzo IAM (OIDC-based).
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the HanzoIamProvider and IamProvider implementations
|
|
5
|
+
* so all Next.js apps can share one canonical implementation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // next-auth config
|
|
10
|
+
* import { HanzoIamProvider } from "@hanzo/iam/nextauth";
|
|
11
|
+
*
|
|
12
|
+
* export default NextAuth({
|
|
13
|
+
* providers: [
|
|
14
|
+
* HanzoIamProvider({
|
|
15
|
+
* serverUrl: process.env.IAM_SERVER_URL!,
|
|
16
|
+
* clientId: process.env.IAM_CLIENT_ID!,
|
|
17
|
+
* clientSecret: process.env.IAM_CLIENT_SECRET!,
|
|
18
|
+
* }),
|
|
19
|
+
* ],
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface HanzoIamProfile extends Record<string, unknown> {
|
|
27
|
+
sub: string;
|
|
28
|
+
name: string;
|
|
29
|
+
email: string;
|
|
30
|
+
preferred_username?: string;
|
|
31
|
+
picture?: string;
|
|
32
|
+
avatar?: string;
|
|
33
|
+
displayName?: string;
|
|
34
|
+
email_verified?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* NextAuth.js / Auth.js compatible OAuth provider for Hanzo IAM.
|
|
39
|
+
*
|
|
40
|
+
* Uses standard OIDC well-known endpoint for automatic configuration.
|
|
41
|
+
* JWT id_token validation (issuer, audience, signature) is handled by
|
|
42
|
+
* openid-client using the JWKS published at `{serverUrl}/.well-known/jwks`.
|
|
43
|
+
*
|
|
44
|
+
* Pass `checks: ["state", "pkce"]` in options for PKCE alignment.
|
|
45
|
+
*/
|
|
46
|
+
export function HanzoIamProvider<P extends HanzoIamProfile>(
|
|
47
|
+
options: {
|
|
48
|
+
serverUrl: string;
|
|
49
|
+
clientId: string;
|
|
50
|
+
clientSecret?: string;
|
|
51
|
+
orgName?: string;
|
|
52
|
+
appName?: string;
|
|
53
|
+
/** OAuth state/PKCE checks. Default: ["state"]. Add "pkce" for extra security. */
|
|
54
|
+
checks?: ("state" | "pkce" | "nonce" | "none")[];
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
},
|
|
57
|
+
): Record<string, unknown> {
|
|
58
|
+
const issuer = options.serverUrl.replace(/\/$/, "");
|
|
59
|
+
const checks = options.checks ?? ["state"];
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
id: "hanzo-iam",
|
|
63
|
+
name: "Hanzo IAM",
|
|
64
|
+
type: "oauth",
|
|
65
|
+
wellKnown: `${issuer}/.well-known/openid-configuration`,
|
|
66
|
+
idToken: true,
|
|
67
|
+
checks,
|
|
68
|
+
authorization: { params: { scope: "openid profile email" } },
|
|
69
|
+
profile(profile: P) {
|
|
70
|
+
return {
|
|
71
|
+
id: profile.sub,
|
|
72
|
+
name:
|
|
73
|
+
profile.displayName ||
|
|
74
|
+
profile.name ||
|
|
75
|
+
profile.preferred_username ||
|
|
76
|
+
profile.email ||
|
|
77
|
+
"",
|
|
78
|
+
email: profile.email,
|
|
79
|
+
image: profile.avatar || profile.picture || null,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
style: {
|
|
83
|
+
bg: "#050508",
|
|
84
|
+
text: "#fff",
|
|
85
|
+
logo: "",
|
|
86
|
+
},
|
|
87
|
+
options,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Re-export with alias for backwards compat
|
|
92
|
+
export { HanzoIamProvider as IamProvider };
|
|
93
|
+
export type { HanzoIamProfile };
|