@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/src/billing.ts CHANGED
@@ -1,72 +1,118 @@
1
1
  /**
2
- * Billing client for Hanzo IAM (Casdoor) — subscriptions, plans, pricing, usage.
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
- export class IamBillingClient {
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 readonly clientId: string;
20
- private readonly clientSecret: string | undefined;
21
- private readonly orgName: string | undefined;
89
+ private token: string | undefined;
22
90
 
23
- constructor(config: IamConfig) {
24
- this.baseUrl = config.serverUrl.replace(/\/+$/, "");
25
- this.clientId = config.clientId;
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
- // Internal HTTP helper
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
- Accept: "application/json",
55
- };
56
- if (opts?.token) {
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 Error(`IAM billing request failed (${res.status}): ${text}`.trim());
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
- // Subscriptions
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
- let plan: IamPlan | null = null;
232
- if (sub.plan) {
233
- plan = await this.getPlan(sub.plan, token);
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
- return { active: true, subscription: sub, plan };
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 (Casdoor-based identity & access management).
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, IamBillingClient, validateToken } from "@hanzo/iam";
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
- * // Validate a JWT
14
- * const result = await validateToken(accessToken, {
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";
@@ -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 };