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 +1 -1
- package/dist/react/index.js +66 -0
- package/dist/react/index.mjs +66 -0
- package/dist/server/index.d.mts +8 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.js +9 -0
- package/dist/server/index.mjs +9 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
package/dist/react/index.js
CHANGED
|
@@ -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",
|
package/dist/react/index.mjs
CHANGED
|
@@ -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",
|
package/dist/server/index.d.mts
CHANGED
|
@@ -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>;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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>;
|
package/dist/server/index.js
CHANGED
|
@@ -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) {
|
package/dist/server/index.mjs
CHANGED
|
@@ -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) {
|