apiblaze 0.17.21 → 0.17.23
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/README.md +63 -0
- package/dist/index.js +451 -217
- package/dist/react/index.d.mts +67 -7
- package/dist/react/index.d.ts +67 -7
- package/dist/react/index.js +651 -69
- package/dist/react/index.mjs +651 -69
- package/dist/server/index.d.mts +165 -25
- package/dist/server/index.d.ts +165 -25
- package/dist/server/index.js +207 -35
- package/dist/server/index.mjs +204 -36
- package/package.json +28 -16
package/dist/server/index.js
CHANGED
|
@@ -20,15 +20,38 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/server/index.ts
|
|
21
21
|
var server_exports = {};
|
|
22
22
|
__export(server_exports, {
|
|
23
|
+
PlaneError: () => PlaneError,
|
|
24
|
+
createApiblazeGroups: () => createApiblazeGroups,
|
|
23
25
|
createApiblazeKeys: () => createApiblazeKeys
|
|
24
26
|
});
|
|
25
27
|
module.exports = __toCommonJS(server_exports);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
|
|
29
|
+
// src/server/core.ts
|
|
30
|
+
var PlaneError = class extends Error {
|
|
31
|
+
constructor(status, detail) {
|
|
32
|
+
super(detail);
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.detail = detail;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
async function safeErr(res) {
|
|
38
|
+
try {
|
|
39
|
+
const b = await res.json();
|
|
40
|
+
return typeof b.error === "string" ? b.error : b.error?.message ?? `apiblaze ${res.status}`;
|
|
41
|
+
} catch {
|
|
42
|
+
return `apiblaze ${res.status}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function json(data, status = 200) {
|
|
46
|
+
return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json" } });
|
|
47
|
+
}
|
|
48
|
+
function createPlaneCore(cfg) {
|
|
49
|
+
if (!cfg.cpKey) throw new Error("apiblaze: cpKey is required (set APIBLAZE_CP_KEY). It is server-only \u2014 never expose it to the browser.");
|
|
50
|
+
const apikeysBase = (cfg.apikeysBase ?? "https://apikeys.apiblaze.com").replace(/\/$/, "");
|
|
51
|
+
const iamBase = (cfg.iamBase ?? "https://iam.apiblaze.com").replace(/\/$/, "");
|
|
30
52
|
const tenantCache = /* @__PURE__ */ new Map();
|
|
31
|
-
|
|
53
|
+
const endUserCache = /* @__PURE__ */ new Map();
|
|
54
|
+
function planeFetch(base, method, path, user, opts = {}) {
|
|
32
55
|
const params = new URLSearchParams({ product: "apiblaze", ...opts.query ?? {} });
|
|
33
56
|
const headers = { "X-API-Key": cfg.cpKey, "X-End-User-Id": user.userId };
|
|
34
57
|
if (user.email) headers["X-End-User-Email"] = String(user.email);
|
|
@@ -42,26 +65,191 @@ function createApiblazeKeys(cfg) {
|
|
|
42
65
|
async function resolveTenant(user) {
|
|
43
66
|
const cached = tenantCache.get(user.tenant);
|
|
44
67
|
if (cached) return cached;
|
|
45
|
-
const res = await planeFetch("POST", "/tenants", user, { body: { ref: user.tenant } });
|
|
68
|
+
const res = await planeFetch(apikeysBase, "POST", "/tenants", user, { body: { ref: user.tenant } });
|
|
46
69
|
if (!res.ok) throw new PlaneError(res.status, await safeErr(res));
|
|
47
70
|
const { tenant } = await res.json();
|
|
48
71
|
tenantCache.set(user.tenant, tenant);
|
|
49
72
|
return tenant;
|
|
50
73
|
}
|
|
74
|
+
async function ensureEndUser(user, tenant) {
|
|
75
|
+
const cacheKey = `${tenant}:${user.userId}`;
|
|
76
|
+
const cached = endUserCache.get(cacheKey);
|
|
77
|
+
if (cached) return cached;
|
|
78
|
+
const res = await planeFetch(apikeysBase, "POST", "/users", user, {
|
|
79
|
+
body: { tenant, ref: user.userId, email: user.email ?? null, display_name: user.label ?? null }
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok) throw new PlaneError(res.status, await safeErr(res));
|
|
82
|
+
const { abz_sub } = await res.json();
|
|
83
|
+
endUserCache.set(cacheKey, abz_sub);
|
|
84
|
+
return abz_sub;
|
|
85
|
+
}
|
|
86
|
+
return { planeFetch, resolveTenant, ensureEndUser, apikeysBase, iamBase };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/server/groups.ts
|
|
90
|
+
function createApiblazeGroups(cfg) {
|
|
91
|
+
if (typeof cfg.getUser !== "function") throw new Error("apiblaze: getUser is required \u2014 map your session to { tenant, userId, email }.");
|
|
92
|
+
const core = createPlaneCore({ cpKey: cfg.cpKey, apikeysBase: cfg.base, iamBase: cfg.iamBase });
|
|
93
|
+
const iam = (method, path, user, tenant, body) => core.planeFetch(core.iamBase, method, `/v1/admin${path}`, user, { query: { tenant }, body });
|
|
94
|
+
async function context(user) {
|
|
95
|
+
const tenant = await core.resolveTenant(user);
|
|
96
|
+
await core.ensureEndUser(user, tenant);
|
|
97
|
+
return { tenant };
|
|
98
|
+
}
|
|
99
|
+
async function pass(r) {
|
|
100
|
+
if (r.status === 204) return { status: 200, data: { ok: true } };
|
|
101
|
+
return { status: r.status, data: await r.json().catch(() => ({})) };
|
|
102
|
+
}
|
|
51
103
|
async function run(user, op) {
|
|
52
|
-
const tenant = await
|
|
104
|
+
const { tenant } = await context(user);
|
|
105
|
+
if (op.action === "snapshot") {
|
|
106
|
+
const [groupsRes, usersRes, observedRes] = await Promise.all([
|
|
107
|
+
iam("GET", "/groups", user, tenant),
|
|
108
|
+
iam("GET", "/users", user, tenant),
|
|
109
|
+
iam("GET", "/observed-identities", user, tenant)
|
|
110
|
+
]);
|
|
111
|
+
if (groupsRes.status === 403) return { status: 200, data: { access: "pending", groups: [], users: [], observed: [] } };
|
|
112
|
+
if (!groupsRes.ok) return { status: groupsRes.status, data: { error: await safeErr(groupsRes) } };
|
|
113
|
+
const g = await groupsRes.json();
|
|
114
|
+
const u = usersRes.ok ? await usersRes.json() : { users: [] };
|
|
115
|
+
const o = observedRes.ok ? await observedRes.json() : {};
|
|
116
|
+
return {
|
|
117
|
+
status: 200,
|
|
118
|
+
data: { access: "ok", groups: g.groups ?? [], users: u.users ?? [], observed: o.identities ?? o.observed ?? [] }
|
|
119
|
+
};
|
|
120
|
+
}
|
|
53
121
|
switch (op.action) {
|
|
54
|
-
case "
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
122
|
+
case "group-detail": {
|
|
123
|
+
const [detail, subs] = await Promise.all([
|
|
124
|
+
iam("GET", `/groups/${encodeURIComponent(op.groupId)}`, user, tenant),
|
|
125
|
+
iam("GET", `/groups/${encodeURIComponent(op.groupId)}/subgroups`, user, tenant)
|
|
126
|
+
]);
|
|
127
|
+
if (!detail.ok) return pass(detail);
|
|
128
|
+
const d = await detail.json();
|
|
129
|
+
const s = subs.ok ? await subs.json() : { subgroups: [] };
|
|
130
|
+
return { status: 200, data: { ...d, subgroups: s.subgroups ?? [] } };
|
|
58
131
|
}
|
|
59
|
-
case "create":
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
132
|
+
case "create-group":
|
|
133
|
+
return pass(await iam("POST", "/groups", user, tenant, {
|
|
134
|
+
name: op.name,
|
|
135
|
+
description: op.description,
|
|
136
|
+
admins: [await core.ensureEndUser(user, tenant)]
|
|
137
|
+
}));
|
|
138
|
+
case "rename-group":
|
|
139
|
+
return pass(await iam("PATCH", `/groups/${encodeURIComponent(op.groupId)}`, user, tenant, {
|
|
140
|
+
name: op.name,
|
|
141
|
+
description: op.description
|
|
142
|
+
}));
|
|
143
|
+
case "delete-group":
|
|
144
|
+
return pass(await iam("DELETE", `/groups/${encodeURIComponent(op.groupId)}`, user, tenant));
|
|
145
|
+
case "add-member":
|
|
146
|
+
return pass(await iam("POST", `/groups/${encodeURIComponent(op.groupId)}/members`, user, tenant, {
|
|
147
|
+
abz_sub: op.abzSub,
|
|
148
|
+
role: op.role ?? "member"
|
|
149
|
+
}));
|
|
150
|
+
case "change-role":
|
|
151
|
+
return pass(await iam("PATCH", `/groups/${encodeURIComponent(op.groupId)}/members/_`, user, tenant, {
|
|
152
|
+
abz_sub: op.abzSub,
|
|
153
|
+
role: op.role
|
|
154
|
+
}));
|
|
155
|
+
case "remove-member":
|
|
156
|
+
return pass(await core.planeFetch(
|
|
157
|
+
core.iamBase,
|
|
158
|
+
"DELETE",
|
|
159
|
+
`/v1/admin/groups/${encodeURIComponent(op.groupId)}/members/_`,
|
|
160
|
+
user,
|
|
161
|
+
{ query: { tenant, abz_sub: op.abzSub } }
|
|
162
|
+
));
|
|
163
|
+
case "add-subgroup":
|
|
164
|
+
return pass(await iam("POST", `/groups/${encodeURIComponent(op.groupId)}/subgroups`, user, tenant, {
|
|
165
|
+
child_group_id: op.childGroupId
|
|
166
|
+
}));
|
|
167
|
+
case "remove-subgroup":
|
|
168
|
+
return pass(await iam("DELETE", `/groups/${encodeURIComponent(op.groupId)}/subgroups/${encodeURIComponent(op.childGroupId)}`, user, tenant));
|
|
169
|
+
case "provision-observed":
|
|
170
|
+
return pass(await iam("POST", "/identities/upsert", user, tenant, {
|
|
171
|
+
consumer_user_id: op.consumerUserId,
|
|
172
|
+
email: op.email,
|
|
173
|
+
display_name: op.displayName,
|
|
174
|
+
groups: op.groups ?? []
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function handler(req) {
|
|
179
|
+
let user;
|
|
180
|
+
try {
|
|
181
|
+
user = await cfg.getUser(req);
|
|
182
|
+
} catch {
|
|
183
|
+
user = null;
|
|
184
|
+
}
|
|
185
|
+
if (!user || !user.tenant || !user.userId) return json({ error: "Unauthorized" }, 401);
|
|
186
|
+
let op;
|
|
187
|
+
if (req.method === "GET") op = { action: "snapshot" };
|
|
188
|
+
else op = await req.json().catch(() => ({}));
|
|
189
|
+
if (!op || typeof op.action !== "string") return json({ error: "action required" }, 400);
|
|
190
|
+
try {
|
|
191
|
+
const { status, data } = await run(user, op);
|
|
192
|
+
return json(data, status);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
if (e instanceof PlaneError) return json({ error: e.detail }, e.status);
|
|
195
|
+
return json({ error: e instanceof Error ? e.message : "Unknown error" }, 500);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { handler, run };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/server/index.ts
|
|
202
|
+
function createApiblazeKeys(cfg) {
|
|
203
|
+
if (typeof cfg.getUser !== "function") throw new Error("apiblaze: getUser is required \u2014 map your session to { tenant, userId }.");
|
|
204
|
+
const core = createPlaneCore({ cpKey: cfg.cpKey, apikeysBase: cfg.base });
|
|
205
|
+
const base = core.apikeysBase;
|
|
206
|
+
const expiresIn = cfg.keyExpiresInSeconds;
|
|
207
|
+
const planeFetch = (method, path, user, opts = {}) => core.planeFetch(base, method, path, user, opts);
|
|
208
|
+
const resolveTenant = (user) => core.resolveTenant(user);
|
|
209
|
+
async function allowedTypesFor(user) {
|
|
210
|
+
const raw = cfg.resolveKeyTypes ? await cfg.resolveKeyTypes(user) : user.keyTypes;
|
|
211
|
+
if (raw === false) return false;
|
|
212
|
+
if (Array.isArray(raw)) return raw.length === 0 ? false : raw;
|
|
213
|
+
return ["call-only"];
|
|
214
|
+
}
|
|
215
|
+
async function catalogNames(user, tenant) {
|
|
216
|
+
const r = await planeFetch("GET", "/key-types", user, { query: { tenant } });
|
|
217
|
+
if (!r.ok) return [];
|
|
218
|
+
const d = await r.json();
|
|
219
|
+
return (d.key_types ?? []).map((t) => t.name).filter((n) => typeof n === "string");
|
|
220
|
+
}
|
|
221
|
+
async function run(user, op) {
|
|
222
|
+
if (op.action === "list") {
|
|
223
|
+
const allowed = await allowedTypesFor(user);
|
|
224
|
+
if (allowed === false) return { status: 200, data: { keys: [], keyTypes: [], access: "denied" } };
|
|
225
|
+
const tenant2 = await resolveTenant(user);
|
|
226
|
+
const [listRes, catalog] = await Promise.all([
|
|
227
|
+
planeFetch("GET", "/apikeys", user, { query: { tenant: tenant2, limit: "200" } }),
|
|
228
|
+
catalogNames(user, tenant2)
|
|
229
|
+
]);
|
|
230
|
+
const d = await listRes.json();
|
|
231
|
+
const eligible = allowed.filter((t) => catalog.includes(t));
|
|
232
|
+
return { status: listRes.status, data: { keys: d.keys ?? [], keyTypes: eligible, access: "ok" } };
|
|
233
|
+
}
|
|
234
|
+
if (op.action === "create") {
|
|
235
|
+
const allowed = await allowedTypesFor(user);
|
|
236
|
+
if (allowed === false) return { status: 403, data: { error: "You do not have API access.", access: "denied" } };
|
|
237
|
+
const tenant2 = await resolveTenant(user);
|
|
238
|
+
const catalog = await catalogNames(user, tenant2);
|
|
239
|
+
const eligible = allowed.filter((t) => catalog.includes(t));
|
|
240
|
+
if (eligible.length === 0) return { status: 409, data: { error: "No key types are available for your account." } };
|
|
241
|
+
if (op.keyType && !eligible.includes(op.keyType)) {
|
|
242
|
+
return { status: 403, data: { error: "You are not allowed to create that key type." } };
|
|
64
243
|
}
|
|
244
|
+
const chosen = op.keyType ?? (eligible.includes("call-only") ? "call-only" : eligible[0]);
|
|
245
|
+
const body = { tenant: tenant2, key_type_ids: [chosen], environment: "prod" };
|
|
246
|
+
if (typeof expiresIn === "number" && expiresIn > 0) body.expires_in_seconds = expiresIn;
|
|
247
|
+
const r = await planeFetch("POST", "/apikeys", user, { body });
|
|
248
|
+
const d = await r.json();
|
|
249
|
+
return { status: r.status, data: { key: d.key, key_id: d.metadata?.key_id } };
|
|
250
|
+
}
|
|
251
|
+
const tenant = await resolveTenant(user);
|
|
252
|
+
switch (op.action) {
|
|
65
253
|
case "reveal": {
|
|
66
254
|
const r = await planeFetch("GET", `/apikeys/${encodeURIComponent(op.keyId)}/reveal`, user, { query: { tenant } });
|
|
67
255
|
return { status: r.status, data: await r.json().catch(() => ({})) };
|
|
@@ -91,7 +279,7 @@ function createApiblazeKeys(cfg) {
|
|
|
91
279
|
const raw = await req.json().catch(() => ({}));
|
|
92
280
|
const a = raw.action ?? "create";
|
|
93
281
|
if ((a === "reveal" || a === "rotate" || a === "revoke") && !raw.keyId) return json({ error: "keyId required" }, 400);
|
|
94
|
-
op = { action: a, keyId: raw.keyId };
|
|
282
|
+
op = { action: a, keyId: raw.keyId, keyType: raw.keyType };
|
|
95
283
|
}
|
|
96
284
|
try {
|
|
97
285
|
const { status, data } = await run(user, op);
|
|
@@ -103,25 +291,9 @@ function createApiblazeKeys(cfg) {
|
|
|
103
291
|
}
|
|
104
292
|
return { handler, run };
|
|
105
293
|
}
|
|
106
|
-
var PlaneError = class extends Error {
|
|
107
|
-
constructor(status, detail) {
|
|
108
|
-
super(detail);
|
|
109
|
-
this.status = status;
|
|
110
|
-
this.detail = detail;
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
async function safeErr(res) {
|
|
114
|
-
try {
|
|
115
|
-
const b = await res.json();
|
|
116
|
-
return typeof b.error === "string" ? b.error : b.error?.message ?? `apiblaze ${res.status}`;
|
|
117
|
-
} catch {
|
|
118
|
-
return `apiblaze ${res.status}`;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function json(data, status = 200) {
|
|
122
|
-
return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json" } });
|
|
123
|
-
}
|
|
124
294
|
// Annotate the CommonJS export names for ESM import in node:
|
|
125
295
|
0 && (module.exports = {
|
|
296
|
+
PlaneError,
|
|
297
|
+
createApiblazeGroups,
|
|
126
298
|
createApiblazeKeys
|
|
127
299
|
});
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
-
// src/server/
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// src/server/core.ts
|
|
2
|
+
var PlaneError = class extends Error {
|
|
3
|
+
constructor(status, detail) {
|
|
4
|
+
super(detail);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.detail = detail;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
async function safeErr(res) {
|
|
10
|
+
try {
|
|
11
|
+
const b = await res.json();
|
|
12
|
+
return typeof b.error === "string" ? b.error : b.error?.message ?? `apiblaze ${res.status}`;
|
|
13
|
+
} catch {
|
|
14
|
+
return `apiblaze ${res.status}`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function json(data, status = 200) {
|
|
18
|
+
return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json" } });
|
|
19
|
+
}
|
|
20
|
+
function createPlaneCore(cfg) {
|
|
21
|
+
if (!cfg.cpKey) throw new Error("apiblaze: cpKey is required (set APIBLAZE_CP_KEY). It is server-only \u2014 never expose it to the browser.");
|
|
22
|
+
const apikeysBase = (cfg.apikeysBase ?? "https://apikeys.apiblaze.com").replace(/\/$/, "");
|
|
23
|
+
const iamBase = (cfg.iamBase ?? "https://iam.apiblaze.com").replace(/\/$/, "");
|
|
6
24
|
const tenantCache = /* @__PURE__ */ new Map();
|
|
7
|
-
|
|
25
|
+
const endUserCache = /* @__PURE__ */ new Map();
|
|
26
|
+
function planeFetch(base, method, path, user, opts = {}) {
|
|
8
27
|
const params = new URLSearchParams({ product: "apiblaze", ...opts.query ?? {} });
|
|
9
28
|
const headers = { "X-API-Key": cfg.cpKey, "X-End-User-Id": user.userId };
|
|
10
29
|
if (user.email) headers["X-End-User-Email"] = String(user.email);
|
|
@@ -18,26 +37,191 @@ function createApiblazeKeys(cfg) {
|
|
|
18
37
|
async function resolveTenant(user) {
|
|
19
38
|
const cached = tenantCache.get(user.tenant);
|
|
20
39
|
if (cached) return cached;
|
|
21
|
-
const res = await planeFetch("POST", "/tenants", user, { body: { ref: user.tenant } });
|
|
40
|
+
const res = await planeFetch(apikeysBase, "POST", "/tenants", user, { body: { ref: user.tenant } });
|
|
22
41
|
if (!res.ok) throw new PlaneError(res.status, await safeErr(res));
|
|
23
42
|
const { tenant } = await res.json();
|
|
24
43
|
tenantCache.set(user.tenant, tenant);
|
|
25
44
|
return tenant;
|
|
26
45
|
}
|
|
46
|
+
async function ensureEndUser(user, tenant) {
|
|
47
|
+
const cacheKey = `${tenant}:${user.userId}`;
|
|
48
|
+
const cached = endUserCache.get(cacheKey);
|
|
49
|
+
if (cached) return cached;
|
|
50
|
+
const res = await planeFetch(apikeysBase, "POST", "/users", user, {
|
|
51
|
+
body: { tenant, ref: user.userId, email: user.email ?? null, display_name: user.label ?? null }
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) throw new PlaneError(res.status, await safeErr(res));
|
|
54
|
+
const { abz_sub } = await res.json();
|
|
55
|
+
endUserCache.set(cacheKey, abz_sub);
|
|
56
|
+
return abz_sub;
|
|
57
|
+
}
|
|
58
|
+
return { planeFetch, resolveTenant, ensureEndUser, apikeysBase, iamBase };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/server/groups.ts
|
|
62
|
+
function createApiblazeGroups(cfg) {
|
|
63
|
+
if (typeof cfg.getUser !== "function") throw new Error("apiblaze: getUser is required \u2014 map your session to { tenant, userId, email }.");
|
|
64
|
+
const core = createPlaneCore({ cpKey: cfg.cpKey, apikeysBase: cfg.base, iamBase: cfg.iamBase });
|
|
65
|
+
const iam = (method, path, user, tenant, body) => core.planeFetch(core.iamBase, method, `/v1/admin${path}`, user, { query: { tenant }, body });
|
|
66
|
+
async function context(user) {
|
|
67
|
+
const tenant = await core.resolveTenant(user);
|
|
68
|
+
await core.ensureEndUser(user, tenant);
|
|
69
|
+
return { tenant };
|
|
70
|
+
}
|
|
71
|
+
async function pass(r) {
|
|
72
|
+
if (r.status === 204) return { status: 200, data: { ok: true } };
|
|
73
|
+
return { status: r.status, data: await r.json().catch(() => ({})) };
|
|
74
|
+
}
|
|
27
75
|
async function run(user, op) {
|
|
28
|
-
const tenant = await
|
|
76
|
+
const { tenant } = await context(user);
|
|
77
|
+
if (op.action === "snapshot") {
|
|
78
|
+
const [groupsRes, usersRes, observedRes] = await Promise.all([
|
|
79
|
+
iam("GET", "/groups", user, tenant),
|
|
80
|
+
iam("GET", "/users", user, tenant),
|
|
81
|
+
iam("GET", "/observed-identities", user, tenant)
|
|
82
|
+
]);
|
|
83
|
+
if (groupsRes.status === 403) return { status: 200, data: { access: "pending", groups: [], users: [], observed: [] } };
|
|
84
|
+
if (!groupsRes.ok) return { status: groupsRes.status, data: { error: await safeErr(groupsRes) } };
|
|
85
|
+
const g = await groupsRes.json();
|
|
86
|
+
const u = usersRes.ok ? await usersRes.json() : { users: [] };
|
|
87
|
+
const o = observedRes.ok ? await observedRes.json() : {};
|
|
88
|
+
return {
|
|
89
|
+
status: 200,
|
|
90
|
+
data: { access: "ok", groups: g.groups ?? [], users: u.users ?? [], observed: o.identities ?? o.observed ?? [] }
|
|
91
|
+
};
|
|
92
|
+
}
|
|
29
93
|
switch (op.action) {
|
|
30
|
-
case "
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
94
|
+
case "group-detail": {
|
|
95
|
+
const [detail, subs] = await Promise.all([
|
|
96
|
+
iam("GET", `/groups/${encodeURIComponent(op.groupId)}`, user, tenant),
|
|
97
|
+
iam("GET", `/groups/${encodeURIComponent(op.groupId)}/subgroups`, user, tenant)
|
|
98
|
+
]);
|
|
99
|
+
if (!detail.ok) return pass(detail);
|
|
100
|
+
const d = await detail.json();
|
|
101
|
+
const s = subs.ok ? await subs.json() : { subgroups: [] };
|
|
102
|
+
return { status: 200, data: { ...d, subgroups: s.subgroups ?? [] } };
|
|
34
103
|
}
|
|
35
|
-
case "create":
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
104
|
+
case "create-group":
|
|
105
|
+
return pass(await iam("POST", "/groups", user, tenant, {
|
|
106
|
+
name: op.name,
|
|
107
|
+
description: op.description,
|
|
108
|
+
admins: [await core.ensureEndUser(user, tenant)]
|
|
109
|
+
}));
|
|
110
|
+
case "rename-group":
|
|
111
|
+
return pass(await iam("PATCH", `/groups/${encodeURIComponent(op.groupId)}`, user, tenant, {
|
|
112
|
+
name: op.name,
|
|
113
|
+
description: op.description
|
|
114
|
+
}));
|
|
115
|
+
case "delete-group":
|
|
116
|
+
return pass(await iam("DELETE", `/groups/${encodeURIComponent(op.groupId)}`, user, tenant));
|
|
117
|
+
case "add-member":
|
|
118
|
+
return pass(await iam("POST", `/groups/${encodeURIComponent(op.groupId)}/members`, user, tenant, {
|
|
119
|
+
abz_sub: op.abzSub,
|
|
120
|
+
role: op.role ?? "member"
|
|
121
|
+
}));
|
|
122
|
+
case "change-role":
|
|
123
|
+
return pass(await iam("PATCH", `/groups/${encodeURIComponent(op.groupId)}/members/_`, user, tenant, {
|
|
124
|
+
abz_sub: op.abzSub,
|
|
125
|
+
role: op.role
|
|
126
|
+
}));
|
|
127
|
+
case "remove-member":
|
|
128
|
+
return pass(await core.planeFetch(
|
|
129
|
+
core.iamBase,
|
|
130
|
+
"DELETE",
|
|
131
|
+
`/v1/admin/groups/${encodeURIComponent(op.groupId)}/members/_`,
|
|
132
|
+
user,
|
|
133
|
+
{ query: { tenant, abz_sub: op.abzSub } }
|
|
134
|
+
));
|
|
135
|
+
case "add-subgroup":
|
|
136
|
+
return pass(await iam("POST", `/groups/${encodeURIComponent(op.groupId)}/subgroups`, user, tenant, {
|
|
137
|
+
child_group_id: op.childGroupId
|
|
138
|
+
}));
|
|
139
|
+
case "remove-subgroup":
|
|
140
|
+
return pass(await iam("DELETE", `/groups/${encodeURIComponent(op.groupId)}/subgroups/${encodeURIComponent(op.childGroupId)}`, user, tenant));
|
|
141
|
+
case "provision-observed":
|
|
142
|
+
return pass(await iam("POST", "/identities/upsert", user, tenant, {
|
|
143
|
+
consumer_user_id: op.consumerUserId,
|
|
144
|
+
email: op.email,
|
|
145
|
+
display_name: op.displayName,
|
|
146
|
+
groups: op.groups ?? []
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function handler(req) {
|
|
151
|
+
let user;
|
|
152
|
+
try {
|
|
153
|
+
user = await cfg.getUser(req);
|
|
154
|
+
} catch {
|
|
155
|
+
user = null;
|
|
156
|
+
}
|
|
157
|
+
if (!user || !user.tenant || !user.userId) return json({ error: "Unauthorized" }, 401);
|
|
158
|
+
let op;
|
|
159
|
+
if (req.method === "GET") op = { action: "snapshot" };
|
|
160
|
+
else op = await req.json().catch(() => ({}));
|
|
161
|
+
if (!op || typeof op.action !== "string") return json({ error: "action required" }, 400);
|
|
162
|
+
try {
|
|
163
|
+
const { status, data } = await run(user, op);
|
|
164
|
+
return json(data, status);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
if (e instanceof PlaneError) return json({ error: e.detail }, e.status);
|
|
167
|
+
return json({ error: e instanceof Error ? e.message : "Unknown error" }, 500);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { handler, run };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/server/index.ts
|
|
174
|
+
function createApiblazeKeys(cfg) {
|
|
175
|
+
if (typeof cfg.getUser !== "function") throw new Error("apiblaze: getUser is required \u2014 map your session to { tenant, userId }.");
|
|
176
|
+
const core = createPlaneCore({ cpKey: cfg.cpKey, apikeysBase: cfg.base });
|
|
177
|
+
const base = core.apikeysBase;
|
|
178
|
+
const expiresIn = cfg.keyExpiresInSeconds;
|
|
179
|
+
const planeFetch = (method, path, user, opts = {}) => core.planeFetch(base, method, path, user, opts);
|
|
180
|
+
const resolveTenant = (user) => core.resolveTenant(user);
|
|
181
|
+
async function allowedTypesFor(user) {
|
|
182
|
+
const raw = cfg.resolveKeyTypes ? await cfg.resolveKeyTypes(user) : user.keyTypes;
|
|
183
|
+
if (raw === false) return false;
|
|
184
|
+
if (Array.isArray(raw)) return raw.length === 0 ? false : raw;
|
|
185
|
+
return ["call-only"];
|
|
186
|
+
}
|
|
187
|
+
async function catalogNames(user, tenant) {
|
|
188
|
+
const r = await planeFetch("GET", "/key-types", user, { query: { tenant } });
|
|
189
|
+
if (!r.ok) return [];
|
|
190
|
+
const d = await r.json();
|
|
191
|
+
return (d.key_types ?? []).map((t) => t.name).filter((n) => typeof n === "string");
|
|
192
|
+
}
|
|
193
|
+
async function run(user, op) {
|
|
194
|
+
if (op.action === "list") {
|
|
195
|
+
const allowed = await allowedTypesFor(user);
|
|
196
|
+
if (allowed === false) return { status: 200, data: { keys: [], keyTypes: [], access: "denied" } };
|
|
197
|
+
const tenant2 = await resolveTenant(user);
|
|
198
|
+
const [listRes, catalog] = await Promise.all([
|
|
199
|
+
planeFetch("GET", "/apikeys", user, { query: { tenant: tenant2, limit: "200" } }),
|
|
200
|
+
catalogNames(user, tenant2)
|
|
201
|
+
]);
|
|
202
|
+
const d = await listRes.json();
|
|
203
|
+
const eligible = allowed.filter((t) => catalog.includes(t));
|
|
204
|
+
return { status: listRes.status, data: { keys: d.keys ?? [], keyTypes: eligible, access: "ok" } };
|
|
205
|
+
}
|
|
206
|
+
if (op.action === "create") {
|
|
207
|
+
const allowed = await allowedTypesFor(user);
|
|
208
|
+
if (allowed === false) return { status: 403, data: { error: "You do not have API access.", access: "denied" } };
|
|
209
|
+
const tenant2 = await resolveTenant(user);
|
|
210
|
+
const catalog = await catalogNames(user, tenant2);
|
|
211
|
+
const eligible = allowed.filter((t) => catalog.includes(t));
|
|
212
|
+
if (eligible.length === 0) return { status: 409, data: { error: "No key types are available for your account." } };
|
|
213
|
+
if (op.keyType && !eligible.includes(op.keyType)) {
|
|
214
|
+
return { status: 403, data: { error: "You are not allowed to create that key type." } };
|
|
40
215
|
}
|
|
216
|
+
const chosen = op.keyType ?? (eligible.includes("call-only") ? "call-only" : eligible[0]);
|
|
217
|
+
const body = { tenant: tenant2, key_type_ids: [chosen], environment: "prod" };
|
|
218
|
+
if (typeof expiresIn === "number" && expiresIn > 0) body.expires_in_seconds = expiresIn;
|
|
219
|
+
const r = await planeFetch("POST", "/apikeys", user, { body });
|
|
220
|
+
const d = await r.json();
|
|
221
|
+
return { status: r.status, data: { key: d.key, key_id: d.metadata?.key_id } };
|
|
222
|
+
}
|
|
223
|
+
const tenant = await resolveTenant(user);
|
|
224
|
+
switch (op.action) {
|
|
41
225
|
case "reveal": {
|
|
42
226
|
const r = await planeFetch("GET", `/apikeys/${encodeURIComponent(op.keyId)}/reveal`, user, { query: { tenant } });
|
|
43
227
|
return { status: r.status, data: await r.json().catch(() => ({})) };
|
|
@@ -67,7 +251,7 @@ function createApiblazeKeys(cfg) {
|
|
|
67
251
|
const raw = await req.json().catch(() => ({}));
|
|
68
252
|
const a = raw.action ?? "create";
|
|
69
253
|
if ((a === "reveal" || a === "rotate" || a === "revoke") && !raw.keyId) return json({ error: "keyId required" }, 400);
|
|
70
|
-
op = { action: a, keyId: raw.keyId };
|
|
254
|
+
op = { action: a, keyId: raw.keyId, keyType: raw.keyType };
|
|
71
255
|
}
|
|
72
256
|
try {
|
|
73
257
|
const { status, data } = await run(user, op);
|
|
@@ -79,24 +263,8 @@ function createApiblazeKeys(cfg) {
|
|
|
79
263
|
}
|
|
80
264
|
return { handler, run };
|
|
81
265
|
}
|
|
82
|
-
var PlaneError = class extends Error {
|
|
83
|
-
constructor(status, detail) {
|
|
84
|
-
super(detail);
|
|
85
|
-
this.status = status;
|
|
86
|
-
this.detail = detail;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
async function safeErr(res) {
|
|
90
|
-
try {
|
|
91
|
-
const b = await res.json();
|
|
92
|
-
return typeof b.error === "string" ? b.error : b.error?.message ?? `apiblaze ${res.status}`;
|
|
93
|
-
} catch {
|
|
94
|
-
return `apiblaze ${res.status}`;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
function json(data, status = 200) {
|
|
98
|
-
return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json" } });
|
|
99
|
-
}
|
|
100
266
|
export {
|
|
267
|
+
PlaneError,
|
|
268
|
+
createApiblazeGroups,
|
|
101
269
|
createApiblazeKeys
|
|
102
270
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apiblaze",
|
|
3
|
-
"version": "0.17.
|
|
4
|
-
"description": "APIblaze CLI
|
|
3
|
+
"version": "0.17.23",
|
|
4
|
+
"description": "APIblaze CLI — Chat with your APIs, Manage your API keys, users and groups with the APIblaze serverless proxy",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"apiblaze",
|
|
7
7
|
"dev-tunnel",
|
|
@@ -21,24 +21,37 @@
|
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./dist/index.js",
|
|
23
23
|
"./sidecar": {
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
"import": {
|
|
25
|
+
"types": "./dist/sidecar/index.d.mts",
|
|
26
|
+
"default": "./dist/sidecar/index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"require": {
|
|
29
|
+
"types": "./dist/sidecar/index.d.ts",
|
|
30
|
+
"default": "./dist/sidecar/index.js"
|
|
31
|
+
}
|
|
28
32
|
},
|
|
29
33
|
"./server": {
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/server/index.d.mts",
|
|
36
|
+
"default": "./dist/server/index.mjs"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/server/index.d.ts",
|
|
40
|
+
"default": "./dist/server/index.js"
|
|
41
|
+
}
|
|
34
42
|
},
|
|
35
43
|
"./react": {
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
"import": {
|
|
45
|
+
"types": "./dist/react/index.d.mts",
|
|
46
|
+
"default": "./dist/react/index.mjs"
|
|
47
|
+
},
|
|
48
|
+
"require": {
|
|
49
|
+
"types": "./dist/react/index.d.ts",
|
|
50
|
+
"default": "./dist/react/index.js"
|
|
51
|
+
}
|
|
40
52
|
}
|
|
41
53
|
},
|
|
54
|
+
"sideEffects": false,
|
|
42
55
|
"files": [
|
|
43
56
|
"dist/",
|
|
44
57
|
"README.md"
|
|
@@ -67,8 +80,7 @@
|
|
|
67
80
|
"@types/ws": "^8.5.10",
|
|
68
81
|
"tsup": "^8.0.0",
|
|
69
82
|
"typescript": "^5.4.0",
|
|
70
|
-
"@types/react": "^18.2.0"
|
|
71
|
-
"react": "^18.2.0"
|
|
83
|
+
"@types/react": "^18.2.0"
|
|
72
84
|
},
|
|
73
85
|
"peerDependencies": {
|
|
74
86
|
"react": ">=17"
|