apiblaze 0.18.2 → 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.2";
706
+ var version = "0.18.3";
707
707
 
708
708
  // src/index.ts
709
709
  init_types();
@@ -96,6 +96,9 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
96
96
  const [addSel, setAddSel] = React.useState("");
97
97
  const [addRole, setAddRole] = React.useState("member");
98
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("");
99
102
  const call = React.useCallback(async (body) => {
100
103
  const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
101
104
  const data = await res.json().catch(() => ({}));
@@ -111,6 +114,10 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
111
114
  setGroups((d.groups ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)));
112
115
  setUsers(d.users ?? []);
113
116
  setObserved(d.observed ?? []);
117
+ if (d.access !== "pending") {
118
+ const a = await call({ action: "list-admins" });
119
+ setAdmins(a.admin_emails ?? []);
120
+ }
114
121
  } catch (e) {
115
122
  setLoadError(e instanceof Error ? e.message : "Could not load groups.");
116
123
  setGroups(null);
@@ -172,6 +179,11 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
172
179
  setCreating(false);
173
180
  }
174
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
+ }
175
187
  const vars = {
176
188
  // @ts-expect-error CSS custom properties
177
189
  "--abz-accent": t.accent,
@@ -439,6 +451,60 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
439
451
  ] })
440
452
  ] }, g.id);
441
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
+ ] }),
442
508
  access === "ok" && groups !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
443
509
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
444
510
  "button",
@@ -61,6 +61,9 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
61
61
  const [addSel, setAddSel] = React.useState("");
62
62
  const [addRole, setAddRole] = React.useState("member");
63
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("");
64
67
  const call = React.useCallback(async (body) => {
65
68
  const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
66
69
  const data = await res.json().catch(() => ({}));
@@ -76,6 +79,10 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
76
79
  setGroups((d.groups ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)));
77
80
  setUsers(d.users ?? []);
78
81
  setObserved(d.observed ?? []);
82
+ if (d.access !== "pending") {
83
+ const a = await call({ action: "list-admins" });
84
+ setAdmins(a.admin_emails ?? []);
85
+ }
79
86
  } catch (e) {
80
87
  setLoadError(e instanceof Error ? e.message : "Could not load groups.");
81
88
  setGroups(null);
@@ -137,6 +144,11 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
137
144
  setCreating(false);
138
145
  }
139
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
+ }
140
152
  const vars = {
141
153
  // @ts-expect-error CSS custom properties
142
154
  "--abz-accent": t.accent,
@@ -404,6 +416,60 @@ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint
404
416
  ] })
405
417
  ] }, g.id);
406
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
+ ] }),
407
473
  access === "ok" && groups !== null && /* @__PURE__ */ jsxs("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
408
474
  /* @__PURE__ */ jsxs(
409
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>;
@@ -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) {
@@ -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.2",
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",