@hanzo/iam 0.1.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/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/auth.d.ts +16 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +130 -0
- package/dist/auth.js.map +1 -0
- package/dist/billing.d.ts +41 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +154 -0
- package/dist/billing.js.map +1 -0
- package/dist/browser.d.ts +83 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +370 -0
- package/dist/browser.js.map +1 -0
- package/dist/client.d.ts +55 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +238 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/pkce.d.ts +13 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +36 -0
- package/dist/pkce.js.map +1 -0
- package/dist/react.d.ts +123 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +422 -0
- package/dist/react.js.map +1 -0
- package/dist/types.d.ts +171 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +93 -0
- package/src/auth.ts +151 -0
- package/src/billing.ts +238 -0
- package/src/browser.ts +451 -0
- package/src/client.ts +316 -0
- package/src/index.ts +52 -0
- package/src/pkce.ts +43 -0
- package/src/react.ts +533 -0
- package/src/types.ts +221 -0
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hanzo/iam",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for Hanzo IAM — OIDC auth, JWT validation, OAuth2 PKCE, user/org/billing APIs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./auth": {
|
|
14
|
+
"types": "./dist/auth.d.ts",
|
|
15
|
+
"import": "./dist/auth.js"
|
|
16
|
+
},
|
|
17
|
+
"./browser": {
|
|
18
|
+
"types": "./dist/browser.d.ts",
|
|
19
|
+
"import": "./dist/browser.js"
|
|
20
|
+
},
|
|
21
|
+
"./billing": {
|
|
22
|
+
"types": "./dist/billing.d.ts",
|
|
23
|
+
"import": "./dist/billing.js"
|
|
24
|
+
},
|
|
25
|
+
"./types": {
|
|
26
|
+
"types": "./dist/types.d.ts",
|
|
27
|
+
"import": "./dist/types.js"
|
|
28
|
+
},
|
|
29
|
+
"./react": {
|
|
30
|
+
"types": "./dist/react.d.ts",
|
|
31
|
+
"import": "./dist/react.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"dev": "tsc --watch",
|
|
43
|
+
"clean": "rm -rf dist",
|
|
44
|
+
"prepare": "npm run clean && npm run build",
|
|
45
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
46
|
+
"test": "node --test --import tsx src/**/*.test.ts"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"jose": "^6.1.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": ">=17"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"react": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^22.19.11",
|
|
61
|
+
"@types/react": "^19.0.0",
|
|
62
|
+
"typescript": "^5.5.0"
|
|
63
|
+
},
|
|
64
|
+
"keywords": [
|
|
65
|
+
"hanzo",
|
|
66
|
+
"iam",
|
|
67
|
+
"casdoor",
|
|
68
|
+
"oidc",
|
|
69
|
+
"oauth2",
|
|
70
|
+
"pkce",
|
|
71
|
+
"auth",
|
|
72
|
+
"jwt",
|
|
73
|
+
"identity",
|
|
74
|
+
"access-management",
|
|
75
|
+
"sso"
|
|
76
|
+
],
|
|
77
|
+
"license": "MIT",
|
|
78
|
+
"repository": {
|
|
79
|
+
"type": "git",
|
|
80
|
+
"url": "https://github.com/hanzo-js/iam"
|
|
81
|
+
},
|
|
82
|
+
"homepage": "https://docs.hanzo.ai/services/iam/sdk",
|
|
83
|
+
"bugs": {
|
|
84
|
+
"url": "https://github.com/hanzo-js/iam/issues"
|
|
85
|
+
},
|
|
86
|
+
"author": "Hanzo AI <engineering@hanzo.ai>",
|
|
87
|
+
"publishConfig": {
|
|
88
|
+
"access": "public"
|
|
89
|
+
},
|
|
90
|
+
"engines": {
|
|
91
|
+
"node": ">=18"
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT validation using jose library + OIDC JWKS discovery.
|
|
3
|
+
*
|
|
4
|
+
* Validates access/ID tokens issued by Hanzo IAM (Casdoor).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
|
|
8
|
+
import type { IamConfig, IamAuthResult, IamJwtClaims } from "./types.js";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// JWKS key set cache (per issuer)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const jwksSets = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
|
|
15
|
+
|
|
16
|
+
function getJwksKeySet(jwksUri: string): ReturnType<typeof createRemoteJWKSet> {
|
|
17
|
+
let keySet = jwksSets.get(jwksUri);
|
|
18
|
+
if (!keySet) {
|
|
19
|
+
keySet = createRemoteJWKSet(new URL(jwksUri));
|
|
20
|
+
jwksSets.set(jwksUri, keySet);
|
|
21
|
+
}
|
|
22
|
+
return keySet;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Clear cached JWKS key sets (useful for testing or key rotation). */
|
|
26
|
+
export function clearJwksCache(): void {
|
|
27
|
+
jwksSets.clear();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// OIDC discovery cache (lightweight, no full client needed)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
type CachedDiscovery = { jwksUri: string; issuer: string; fetchedAt: number };
|
|
35
|
+
const discoveryCache = new Map<string, CachedDiscovery>();
|
|
36
|
+
const DISCOVERY_TTL_MS = 5 * 60 * 1000;
|
|
37
|
+
|
|
38
|
+
async function resolveJwksUri(serverUrl: string): Promise<{ jwksUri: string; issuer: string }> {
|
|
39
|
+
const baseUrl = serverUrl.replace(/\/+$/, "");
|
|
40
|
+
const cached = discoveryCache.get(baseUrl);
|
|
41
|
+
if (cached && Date.now() - cached.fetchedAt < DISCOVERY_TTL_MS) {
|
|
42
|
+
return { jwksUri: cached.jwksUri, issuer: cached.issuer };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timer = setTimeout(() => controller.abort(), 8_000);
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${baseUrl}/.well-known/openid-configuration`, {
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
headers: { Accept: "application/json" },
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
throw new Error(`OIDC discovery failed: ${res.status}`);
|
|
54
|
+
}
|
|
55
|
+
const body = (await res.json()) as { jwks_uri?: string; issuer?: string };
|
|
56
|
+
const jwksUri = body.jwks_uri;
|
|
57
|
+
const issuer = body.issuer ?? baseUrl;
|
|
58
|
+
if (!jwksUri) {
|
|
59
|
+
throw new Error("OIDC discovery response missing jwks_uri");
|
|
60
|
+
}
|
|
61
|
+
discoveryCache.set(baseUrl, { jwksUri, issuer, fetchedAt: Date.now() });
|
|
62
|
+
return { jwksUri, issuer };
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Token validation
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate a JWT access token against IAM's JWKS.
|
|
74
|
+
*
|
|
75
|
+
* Uses OIDC discovery to find the JWKS URI, then verifies the token
|
|
76
|
+
* signature, issuer, audience, and expiry using the `jose` library.
|
|
77
|
+
*/
|
|
78
|
+
export async function validateToken(
|
|
79
|
+
token: string,
|
|
80
|
+
config: IamConfig,
|
|
81
|
+
): Promise<IamAuthResult> {
|
|
82
|
+
if (!token || typeof token !== "string") {
|
|
83
|
+
return { ok: false, reason: "iam_token_missing" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let jwksUri: string;
|
|
87
|
+
let issuer: string;
|
|
88
|
+
try {
|
|
89
|
+
const discovery = await resolveJwksUri(config.serverUrl);
|
|
90
|
+
jwksUri = discovery.jwksUri;
|
|
91
|
+
issuer = discovery.issuer;
|
|
92
|
+
} catch {
|
|
93
|
+
return { ok: false, reason: "iam_discovery_failed" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const keySet = getJwksKeySet(jwksUri);
|
|
97
|
+
|
|
98
|
+
let payload: JWTPayload;
|
|
99
|
+
try {
|
|
100
|
+
const result = await jwtVerify(token, keySet, {
|
|
101
|
+
issuer,
|
|
102
|
+
audience: config.clientId,
|
|
103
|
+
clockTolerance: 30, // 30s clock skew
|
|
104
|
+
});
|
|
105
|
+
payload = result.payload;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
108
|
+
if (message.includes("expired")) {
|
|
109
|
+
return { ok: false, reason: "iam_token_expired" };
|
|
110
|
+
}
|
|
111
|
+
if (message.includes("audience")) {
|
|
112
|
+
// Retry without audience check - some Casdoor configs don't set aud
|
|
113
|
+
try {
|
|
114
|
+
const result = await jwtVerify(token, keySet, {
|
|
115
|
+
issuer,
|
|
116
|
+
clockTolerance: 30,
|
|
117
|
+
});
|
|
118
|
+
payload = result.payload;
|
|
119
|
+
} catch {
|
|
120
|
+
return { ok: false, reason: "iam_signature_invalid" };
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
return { ok: false, reason: "iam_signature_invalid" };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const claims = payload as unknown as IamJwtClaims;
|
|
128
|
+
|
|
129
|
+
if (!claims.sub) {
|
|
130
|
+
return { ok: false, reason: "iam_subject_missing" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Casdoor sub format is "org/username" - extract owner
|
|
134
|
+
const parts = claims.sub.split("/");
|
|
135
|
+
const owner = parts.length > 1 ? parts[0] : config.orgName ?? "unknown";
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
ok: true,
|
|
139
|
+
userId: claims.sub,
|
|
140
|
+
email: typeof claims.email === "string" ? claims.email : undefined,
|
|
141
|
+
name:
|
|
142
|
+
typeof claims.name === "string"
|
|
143
|
+
? claims.name
|
|
144
|
+
: typeof claims.preferred_username === "string"
|
|
145
|
+
? claims.preferred_username
|
|
146
|
+
: undefined,
|
|
147
|
+
avatar: typeof claims.picture === "string" ? claims.picture : undefined,
|
|
148
|
+
owner,
|
|
149
|
+
claims,
|
|
150
|
+
};
|
|
151
|
+
}
|
package/src/billing.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing client for Hanzo IAM (Casdoor) — subscriptions, plans, pricing, usage.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
IamConfig,
|
|
7
|
+
IamSubscription,
|
|
8
|
+
IamPlan,
|
|
9
|
+
IamPricing,
|
|
10
|
+
IamPayment,
|
|
11
|
+
IamOrder,
|
|
12
|
+
IamApiResponse,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
16
|
+
|
|
17
|
+
export class IamBillingClient {
|
|
18
|
+
private readonly baseUrl: string;
|
|
19
|
+
private readonly clientId: string;
|
|
20
|
+
private readonly clientSecret: string | undefined;
|
|
21
|
+
private readonly orgName: string | undefined;
|
|
22
|
+
|
|
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;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// -----------------------------------------------------------------------
|
|
31
|
+
// Internal HTTP helper
|
|
32
|
+
// -----------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
private async request<T>(
|
|
35
|
+
path: string,
|
|
36
|
+
opts?: {
|
|
37
|
+
method?: string;
|
|
38
|
+
body?: unknown;
|
|
39
|
+
token?: string;
|
|
40
|
+
params?: Record<string, string>;
|
|
41
|
+
},
|
|
42
|
+
): Promise<T> {
|
|
43
|
+
const url = new URL(path, this.baseUrl);
|
|
44
|
+
if (opts?.params) {
|
|
45
|
+
for (const [k, v] of Object.entries(opts.params)) {
|
|
46
|
+
url.searchParams.set(k, v);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
52
|
+
|
|
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
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(url.toString(), {
|
|
73
|
+
method: opts?.method ?? "GET",
|
|
74
|
+
headers,
|
|
75
|
+
body: opts?.body ? JSON.stringify(opts.body) : undefined,
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
const text = await res.text().catch(() => "");
|
|
81
|
+
throw new Error(`IAM billing request failed (${res.status}): ${text}`.trim());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (await res.json()) as T;
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
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
|
+
}
|
|
230
|
+
|
|
231
|
+
let plan: IamPlan | null = null;
|
|
232
|
+
if (sub.plan) {
|
|
233
|
+
plan = await this.getPlan(sub.plan, token);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { active: true, subscription: sub, plan };
|
|
237
|
+
}
|
|
238
|
+
}
|