apiblaze 0.18.1 → 0.18.3

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/index.js CHANGED
@@ -703,7 +703,7 @@ var import_commander = require("commander");
703
703
  var import_chalk39 = __toESM(require("chalk"));
704
704
 
705
705
  // package.json
706
- var version = "0.18.1";
706
+ var version = "0.18.3";
707
707
 
708
708
  // src/index.ts
709
709
  init_types();
@@ -78,6 +78,7 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
78
78
  const t = { ...DEFAULTS, ...theme ?? {} };
79
79
  const headerBg = t.headerBackground || t.surface;
80
80
  const [access, setAccess] = React.useState("ok");
81
+ const [myEmail, setMyEmail] = React.useState(null);
81
82
  const [groups, setGroups] = React.useState(null);
82
83
  const [users, setUsers] = React.useState([]);
83
84
  const [observed, setObserved] = React.useState([]);
@@ -95,6 +96,9 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
95
96
  const [addSel, setAddSel] = React.useState("");
96
97
  const [addRole, setAddRole] = React.useState("member");
97
98
  const [nestSel, setNestSel] = React.useState("");
99
+ const [admins, setAdmins] = React.useState([]);
100
+ const [showAdmins, setShowAdmins] = React.useState(false);
101
+ const [newAdmin, setNewAdmin] = React.useState("");
98
102
  const call = React.useCallback(async (body) => {
99
103
  const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
100
104
  const data = await res.json().catch(() => ({}));
@@ -106,9 +110,14 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
106
110
  try {
107
111
  const d = await call();
108
112
  setAccess(d.access === "pending" ? "pending" : "ok");
113
+ setMyEmail(d.email ?? null);
109
114
  setGroups((d.groups ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)));
110
115
  setUsers(d.users ?? []);
111
116
  setObserved(d.observed ?? []);
117
+ if (d.access !== "pending") {
118
+ const a = await call({ action: "list-admins" });
119
+ setAdmins(a.admin_emails ?? []);
120
+ }
112
121
  } catch (e) {
113
122
  setLoadError(e instanceof Error ? e.message : "Could not load groups.");
114
123
  setGroups(null);
@@ -170,6 +179,11 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
170
179
  setCreating(false);
171
180
  }
172
181
  }
182
+ async function addAdmin() {
183
+ const email = newAdmin.trim().toLowerCase();
184
+ if (!email) return;
185
+ await act("add-admin", { action: "add-admin", email }, () => setNewAdmin(""));
186
+ }
173
187
  const vars = {
174
188
  // @ts-expect-error CSS custom properties
175
189
  "--abz-accent": t.accent,
@@ -211,10 +225,34 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
211
225
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 15 }, children: title }),
212
226
  access === "ok" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("primary", true), disabled: busy, onClick: () => setShowCreate((v) => !v), "aria-label": "Create a new group", children: "+ New group" })
213
227
  ] }),
214
- access === "pending" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: [
215
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 600, color: t.text, marginBottom: 6 }, children: "Admin access pending" }),
216
- "You don\u2019t have group-management access for this workspace yet. If you were just added as an admin, it activates on your first sign-in \u2014 try refreshing. Otherwise ask your API provider to add your email as a workspace admin.",
217
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginTop: 12 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("ghost", true), onClick: load, children: "Refresh" }) })
228
+ access === "pending" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted, lineHeight: 1.5 }, children: [
229
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 700, color: t.text, marginBottom: 6, fontSize: 15 }, children: "Admin access required" }),
230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: 12 }, children: [
231
+ "Managing users & groups is limited to ",
232
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "workspace admins" }),
233
+ ". You\u2019re signed in",
234
+ myEmail ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
235
+ " as ",
236
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontFamily: t.monoFontFamily }, children: myEmail })
237
+ ] }) : "",
238
+ ", but this account isn\u2019t an admin yet."
239
+ ] }),
240
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { style: { textAlign: "left", maxWidth: 420, margin: "0 auto 12px", paddingLeft: 18 }, children: [
241
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
242
+ "If your API provider just made you an admin, it activates the first time you open this \u2014 ",
243
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Refresh" }),
244
+ "."
245
+ ] }),
246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
247
+ "Otherwise, ask your API provider to add",
248
+ myEmail ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
249
+ " ",
250
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontFamily: t.monoFontFamily }, children: myEmail })
251
+ ] }) : " your email",
252
+ " to the workspace\u2019s admin list."
253
+ ] })
254
+ ] }),
255
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("ghost", true), onClick: load, children: "Refresh" })
218
256
  ] }),
219
257
  access === "ok" && showCreate && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, padding: "12px 16px", borderBottom: `1px solid ${t.border}` }, children: [
220
258
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
@@ -413,6 +451,60 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
413
451
  ] })
414
452
  ] }, g.id);
415
453
  }) }),
454
+ access === "ok" && groups !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
455
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
456
+ "button",
457
+ {
458
+ onClick: () => setShowAdmins((v) => !v),
459
+ "aria-expanded": showAdmins,
460
+ style: { display: "flex", width: "100%", gap: 10, alignItems: "center", padding: "12px 16px", background: "transparent", border: "none", cursor: "pointer", color: t.text, fontFamily: "inherit", fontSize: 13, textAlign: "left" },
461
+ children: [
462
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: { color: t.muted, fontSize: 11 }, children: showAdmins ? "\u25BE" : "\u25B8" }),
463
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { flex: 1 }, children: "Admins" }),
464
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: chip, children: admins.length })
465
+ ]
466
+ }
467
+ ),
468
+ showAdmins && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "0 16px 14px 34px" }, children: [
469
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: [
470
+ admins.map((a) => {
471
+ const removable = !a.pinned && admins.length > 1;
472
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: { display: "flex", gap: 8, alignItems: "center", padding: "6px 0", flexWrap: "wrap" }, children: [
473
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1, minWidth: 160 }, children: a.email }),
474
+ a.pinned && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: chip, title: "Set by the API provider \u2014 you can\u2019t remove this one", children: "provider" }),
475
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...chip, ...a.active ? { color: t.success, borderColor: t.success } : {} }, children: a.active ? "active" : "pending sign-in" }),
476
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
477
+ "button",
478
+ {
479
+ style: { ...btn("danger", true), ...removable ? {} : { opacity: 0.4, cursor: "not-allowed" } },
480
+ disabled: busy || !removable,
481
+ title: a.pinned ? "Provider-set admins can only be removed by the API provider" : admins.length <= 1 ? "A tenant must keep at least one admin" : `Remove ${a.email} as an admin`,
482
+ onClick: () => act(`rm-admin:${a.email}`, { action: "remove-admin", email: a.email }),
483
+ children: "Remove"
484
+ }
485
+ )
486
+ ] }, a.email);
487
+ }),
488
+ admins.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: { color: t.muted, fontSize: 13, padding: "6px 0" }, children: "No admins yet." })
489
+ ] }),
490
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, marginTop: 10 }, children: [
491
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
492
+ "input",
493
+ {
494
+ style: { ...inputStyle, flex: 1 },
495
+ type: "email",
496
+ placeholder: "Add an admin by email",
497
+ value: newAdmin,
498
+ onChange: (e) => setNewAdmin(e.target.value),
499
+ onKeyDown: (e) => e.key === "Enter" && addAdmin(),
500
+ "aria-label": "New admin email"
501
+ }
502
+ ),
503
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("primary", true), disabled: busy || !newAdmin.trim(), onClick: addAdmin, children: "Add admin" })
504
+ ] }),
505
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: t.muted, marginTop: 8 }, children: "A new admin gets access the first time they sign in. Admins are granted by email \u2014 never by adding someone to a group." })
506
+ ] })
507
+ ] }),
416
508
  access === "ok" && groups !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
417
509
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
418
510
  "button",
@@ -43,6 +43,7 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
43
43
  const t = { ...DEFAULTS, ...theme ?? {} };
44
44
  const headerBg = t.headerBackground || t.surface;
45
45
  const [access, setAccess] = React.useState("ok");
46
+ const [myEmail, setMyEmail] = React.useState(null);
46
47
  const [groups, setGroups] = React.useState(null);
47
48
  const [users, setUsers] = React.useState([]);
48
49
  const [observed, setObserved] = React.useState([]);
@@ -60,6 +61,9 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
60
61
  const [addSel, setAddSel] = React.useState("");
61
62
  const [addRole, setAddRole] = React.useState("member");
62
63
  const [nestSel, setNestSel] = React.useState("");
64
+ const [admins, setAdmins] = React.useState([]);
65
+ const [showAdmins, setShowAdmins] = React.useState(false);
66
+ const [newAdmin, setNewAdmin] = React.useState("");
63
67
  const call = React.useCallback(async (body) => {
64
68
  const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
65
69
  const data = await res.json().catch(() => ({}));
@@ -71,9 +75,14 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
71
75
  try {
72
76
  const d = await call();
73
77
  setAccess(d.access === "pending" ? "pending" : "ok");
78
+ setMyEmail(d.email ?? null);
74
79
  setGroups((d.groups ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)));
75
80
  setUsers(d.users ?? []);
76
81
  setObserved(d.observed ?? []);
82
+ if (d.access !== "pending") {
83
+ const a = await call({ action: "list-admins" });
84
+ setAdmins(a.admin_emails ?? []);
85
+ }
77
86
  } catch (e) {
78
87
  setLoadError(e instanceof Error ? e.message : "Could not load groups.");
79
88
  setGroups(null);
@@ -135,6 +144,11 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
135
144
  setCreating(false);
136
145
  }
137
146
  }
147
+ async function addAdmin() {
148
+ const email = newAdmin.trim().toLowerCase();
149
+ if (!email) return;
150
+ await act("add-admin", { action: "add-admin", email }, () => setNewAdmin(""));
151
+ }
138
152
  const vars = {
139
153
  // @ts-expect-error CSS custom properties
140
154
  "--abz-accent": t.accent,
@@ -176,10 +190,34 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
176
190
  /* @__PURE__ */ jsx("strong", { style: { fontSize: 15 }, children: title }),
177
191
  access === "ok" && /* @__PURE__ */ jsx("button", { style: btn("primary", true), disabled: busy, onClick: () => setShowCreate((v) => !v), "aria-label": "Create a new group", children: "+ New group" })
178
192
  ] }),
179
- access === "pending" && /* @__PURE__ */ jsxs("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: [
180
- /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, color: t.text, marginBottom: 6 }, children: "Admin access pending" }),
181
- "You don\u2019t have group-management access for this workspace yet. If you were just added as an admin, it activates on your first sign-in \u2014 try refreshing. Otherwise ask your API provider to add your email as a workspace admin.",
182
- /* @__PURE__ */ jsx("div", { style: { marginTop: 12 }, children: /* @__PURE__ */ jsx("button", { style: btn("ghost", true), onClick: load, children: "Refresh" }) })
193
+ access === "pending" && /* @__PURE__ */ jsxs("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted, lineHeight: 1.5 }, children: [
194
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 700, color: t.text, marginBottom: 6, fontSize: 15 }, children: "Admin access required" }),
195
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
196
+ "Managing users & groups is limited to ",
197
+ /* @__PURE__ */ jsx("strong", { children: "workspace admins" }),
198
+ ". You\u2019re signed in",
199
+ myEmail ? /* @__PURE__ */ jsxs(Fragment, { children: [
200
+ " as ",
201
+ /* @__PURE__ */ jsx("code", { style: { fontFamily: t.monoFontFamily }, children: myEmail })
202
+ ] }) : "",
203
+ ", but this account isn\u2019t an admin yet."
204
+ ] }),
205
+ /* @__PURE__ */ jsxs("ul", { style: { textAlign: "left", maxWidth: 420, margin: "0 auto 12px", paddingLeft: 18 }, children: [
206
+ /* @__PURE__ */ jsxs("li", { children: [
207
+ "If your API provider just made you an admin, it activates the first time you open this \u2014 ",
208
+ /* @__PURE__ */ jsx("strong", { children: "Refresh" }),
209
+ "."
210
+ ] }),
211
+ /* @__PURE__ */ jsxs("li", { children: [
212
+ "Otherwise, ask your API provider to add",
213
+ myEmail ? /* @__PURE__ */ jsxs(Fragment, { children: [
214
+ " ",
215
+ /* @__PURE__ */ jsx("code", { style: { fontFamily: t.monoFontFamily }, children: myEmail })
216
+ ] }) : " your email",
217
+ " to the workspace\u2019s admin list."
218
+ ] })
219
+ ] }),
220
+ /* @__PURE__ */ jsx("button", { style: btn("ghost", true), onClick: load, children: "Refresh" })
183
221
  ] }),
184
222
  access === "ok" && showCreate && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, padding: "12px 16px", borderBottom: `1px solid ${t.border}` }, children: [
185
223
  /* @__PURE__ */ jsx(
@@ -378,6 +416,60 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
378
416
  ] })
379
417
  ] }, g.id);
380
418
  }) }),
419
+ access === "ok" && groups !== null && /* @__PURE__ */ jsxs("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
420
+ /* @__PURE__ */ jsxs(
421
+ "button",
422
+ {
423
+ onClick: () => setShowAdmins((v) => !v),
424
+ "aria-expanded": showAdmins,
425
+ style: { display: "flex", width: "100%", gap: 10, alignItems: "center", padding: "12px 16px", background: "transparent", border: "none", cursor: "pointer", color: t.text, fontFamily: "inherit", fontSize: 13, textAlign: "left" },
426
+ children: [
427
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: t.muted, fontSize: 11 }, children: showAdmins ? "\u25BE" : "\u25B8" }),
428
+ /* @__PURE__ */ jsx("strong", { style: { flex: 1 }, children: "Admins" }),
429
+ /* @__PURE__ */ jsx("span", { style: chip, children: admins.length })
430
+ ]
431
+ }
432
+ ),
433
+ showAdmins && /* @__PURE__ */ jsxs("div", { style: { padding: "0 16px 14px 34px" }, children: [
434
+ /* @__PURE__ */ jsxs("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: [
435
+ admins.map((a) => {
436
+ const removable = !a.pinned && admins.length > 1;
437
+ return /* @__PURE__ */ jsxs("li", { style: { display: "flex", gap: 8, alignItems: "center", padding: "6px 0", flexWrap: "wrap" }, children: [
438
+ /* @__PURE__ */ jsx("span", { style: { flex: 1, minWidth: 160 }, children: a.email }),
439
+ a.pinned && /* @__PURE__ */ jsx("span", { style: chip, title: "Set by the API provider \u2014 you can\u2019t remove this one", children: "provider" }),
440
+ /* @__PURE__ */ jsx("span", { style: { ...chip, ...a.active ? { color: t.success, borderColor: t.success } : {} }, children: a.active ? "active" : "pending sign-in" }),
441
+ /* @__PURE__ */ jsx(
442
+ "button",
443
+ {
444
+ style: { ...btn("danger", true), ...removable ? {} : { opacity: 0.4, cursor: "not-allowed" } },
445
+ disabled: busy || !removable,
446
+ title: a.pinned ? "Provider-set admins can only be removed by the API provider" : admins.length <= 1 ? "A tenant must keep at least one admin" : `Remove ${a.email} as an admin`,
447
+ onClick: () => act(`rm-admin:${a.email}`, { action: "remove-admin", email: a.email }),
448
+ children: "Remove"
449
+ }
450
+ )
451
+ ] }, a.email);
452
+ }),
453
+ admins.length === 0 && /* @__PURE__ */ jsx("li", { style: { color: t.muted, fontSize: 13, padding: "6px 0" }, children: "No admins yet." })
454
+ ] }),
455
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, marginTop: 10 }, children: [
456
+ /* @__PURE__ */ jsx(
457
+ "input",
458
+ {
459
+ style: { ...inputStyle, flex: 1 },
460
+ type: "email",
461
+ placeholder: "Add an admin by email",
462
+ value: newAdmin,
463
+ onChange: (e) => setNewAdmin(e.target.value),
464
+ onKeyDown: (e) => e.key === "Enter" && addAdmin(),
465
+ "aria-label": "New admin email"
466
+ }
467
+ ),
468
+ /* @__PURE__ */ jsx("button", { style: btn("primary", true), disabled: busy || !newAdmin.trim(), onClick: addAdmin, children: "Add admin" })
469
+ ] }),
470
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: t.muted, marginTop: 8 }, children: "A new admin gets access the first time they sign in. Admins are granted by email \u2014 never by adding someone to a group." })
471
+ ] })
472
+ ] }),
381
473
  access === "ok" && groups !== null && /* @__PURE__ */ jsxs("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
382
474
  /* @__PURE__ */ jsxs(
383
475
  "button",
@@ -116,6 +116,14 @@ type GroupsAction = {
116
116
  email?: string;
117
117
  displayName?: string;
118
118
  groups?: string[];
119
+ } | {
120
+ action: 'list-admins';
121
+ } | {
122
+ action: 'add-admin';
123
+ email: string;
124
+ } | {
125
+ action: 'remove-admin';
126
+ email: string;
119
127
  };
120
128
  declare function createApiblazeGroups(cfg: ApiblazeGroupsConfig): {
121
129
  handler: (req: Request) => Promise<Response>;
@@ -116,6 +116,14 @@ type GroupsAction = {
116
116
  email?: string;
117
117
  displayName?: string;
118
118
  groups?: string[];
119
+ } | {
120
+ action: 'list-admins';
121
+ } | {
122
+ action: 'add-admin';
123
+ email: string;
124
+ } | {
125
+ action: 'remove-admin';
126
+ email: string;
119
127
  };
120
128
  declare function createApiblazeGroups(cfg: ApiblazeGroupsConfig): {
121
129
  handler: (req: Request) => Promise<Response>;
@@ -107,7 +107,7 @@ function createApiblazeGroups(cfg) {
107
107
  iam("GET", "/users", user, tenant),
108
108
  iam("GET", "/observed-identities", user, tenant)
109
109
  ]);
110
- if (groupsRes.status === 403) return { status: 200, data: { access: "pending", groups: [], users: [], observed: [] } };
110
+ if (groupsRes.status === 403) return { status: 200, data: { access: "pending", email: user.email ?? null, groups: [], users: [], observed: [] } };
111
111
  if (!groupsRes.ok) return { status: groupsRes.status, data: { error: await safeErr(groupsRes) } };
112
112
  const g = await groupsRes.json();
113
113
  const u = usersRes.ok ? await usersRes.json() : { users: [] };
@@ -172,6 +172,15 @@ function createApiblazeGroups(cfg) {
172
172
  display_name: op.displayName,
173
173
  groups: op.groups ?? []
174
174
  }));
175
+ // Tenant-admin allowlist — appoint/remove CO-admins by email. This is the
176
+ // ONLY way admin membership changes (apiblaze_admins is sealed); the plane
177
+ // enforces the guardrails (producer-pinned + last-admin).
178
+ case "list-admins":
179
+ return pass(await iam("GET", "/admin-emails", user, tenant));
180
+ case "add-admin":
181
+ return pass(await iam("POST", "/admin-emails", user, tenant, { email: op.email }));
182
+ case "remove-admin":
183
+ return pass(await iam("DELETE", `/admin-emails/${encodeURIComponent(op.email)}`, user, tenant));
175
184
  }
176
185
  }
177
186
  async function handler(req) {
@@ -79,7 +79,7 @@ function createApiblazeGroups(cfg) {
79
79
  iam("GET", "/users", user, tenant),
80
80
  iam("GET", "/observed-identities", user, tenant)
81
81
  ]);
82
- if (groupsRes.status === 403) return { status: 200, data: { access: "pending", groups: [], users: [], observed: [] } };
82
+ if (groupsRes.status === 403) return { status: 200, data: { access: "pending", email: user.email ?? null, groups: [], users: [], observed: [] } };
83
83
  if (!groupsRes.ok) return { status: groupsRes.status, data: { error: await safeErr(groupsRes) } };
84
84
  const g = await groupsRes.json();
85
85
  const u = usersRes.ok ? await usersRes.json() : { users: [] };
@@ -144,6 +144,15 @@ function createApiblazeGroups(cfg) {
144
144
  display_name: op.displayName,
145
145
  groups: op.groups ?? []
146
146
  }));
147
+ // Tenant-admin allowlist — appoint/remove CO-admins by email. This is the
148
+ // ONLY way admin membership changes (apiblaze_admins is sealed); the plane
149
+ // enforces the guardrails (producer-pinned + last-admin).
150
+ case "list-admins":
151
+ return pass(await iam("GET", "/admin-emails", user, tenant));
152
+ case "add-admin":
153
+ return pass(await iam("POST", "/admin-emails", user, tenant, { email: op.email }));
154
+ case "remove-admin":
155
+ return pass(await iam("DELETE", `/admin-emails/${encodeURIComponent(op.email)}`, user, tenant));
147
156
  }
148
157
  }
149
158
  async function handler(req) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.18.1",
3
+ "version": "0.18.3",
4
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",