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.
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  "use strict";
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
@@ -31,151 +32,732 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
32
  var react_exports = {};
32
33
  __export(react_exports, {
33
34
  ApiKeyWidget: () => ApiKeyWidget,
35
+ UsersGroupsWidget: () => UsersGroupsWidget,
34
36
  default: () => react_default
35
37
  });
36
38
  module.exports = __toCommonJS(react_exports);
39
+ var React2 = __toESM(require("react"));
40
+
41
+ // src/react/groups.tsx
37
42
  var React = __toESM(require("react"));
38
43
  var import_jsx_runtime = require("react/jsx-runtime");
44
+ var SEALED = "apiblaze_admins";
39
45
  var DEFAULTS = {
40
46
  accent: "#4f46e5",
47
+ accentText: "#ffffff",
41
48
  background: "transparent",
42
49
  surface: "#ffffff",
50
+ headerBackground: "",
43
51
  text: "#111827",
44
52
  muted: "#6b7280",
45
53
  border: "#e5e7eb",
46
- radius: "12px"
54
+ danger: "#dc2626",
55
+ success: "#16a34a",
56
+ radius: "12px",
57
+ fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
58
+ monoFontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
47
59
  };
48
- function ApiKeyWidget({ endpoint = "/api/apiblaze/keys", theme, advanced = true, className }) {
60
+ function userLabel(u) {
61
+ return u.display_name || u.email || u.abz_sub;
62
+ }
63
+ function relTime(iso) {
64
+ if (!iso) return null;
65
+ const t = new Date(iso).getTime();
66
+ if (Number.isNaN(t)) return null;
67
+ const s = Math.floor((Date.now() - t) / 1e3);
68
+ if (s < 60) return "just now";
69
+ const m = Math.floor(s / 60);
70
+ if (m < 60) return `${m}m ago`;
71
+ const h = Math.floor(m / 60);
72
+ if (h < 24) return `${h}h ago`;
73
+ const d = Math.floor(h / 24);
74
+ if (d < 30) return `${d}d ago`;
75
+ return new Date(iso).toLocaleDateString();
76
+ }
77
+ var UsersGroupsWidget = React.forwardRef(function UsersGroupsWidget2({ endpoint = "/api/apiblaze/groups", theme, title = "Users & groups", className }, ref) {
49
78
  const t = { ...DEFAULTS, ...theme ?? {} };
50
- const [keys, setKeys] = React.useState(null);
51
- const [secret, setSecret] = React.useState(null);
52
- const [busy, setBusy] = React.useState(false);
79
+ const headerBg = t.headerBackground || t.surface;
80
+ const [access, setAccess] = React.useState("ok");
81
+ const [groups, setGroups] = React.useState(null);
82
+ const [users, setUsers] = React.useState([]);
83
+ const [observed, setObserved] = React.useState([]);
84
+ const [loadError, setLoadError] = React.useState(null);
85
+ const [open, setOpen] = React.useState(null);
86
+ const [detail, setDetail] = React.useState(null);
87
+ const [detailLoading, setDetailLoading] = React.useState(false);
88
+ const [creating, setCreating] = React.useState(false);
89
+ const [newName, setNewName] = React.useState("");
90
+ const [showCreate, setShowCreate] = React.useState(false);
91
+ const [pending, setPending] = React.useState(null);
53
92
  const [err, setErr] = React.useState(null);
54
- const [open, setOpen] = React.useState(false);
55
- const [copied, setCopied] = React.useState(false);
93
+ const [confirmDelete, setConfirmDelete] = React.useState(null);
94
+ const [showTraffic, setShowTraffic] = React.useState(false);
95
+ const [addSel, setAddSel] = React.useState("");
96
+ const [addRole, setAddRole] = React.useState("member");
97
+ const [nestSel, setNestSel] = React.useState("");
56
98
  const call = React.useCallback(async (body) => {
57
- setErr(null);
58
99
  const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
59
100
  const data = await res.json().catch(() => ({}));
60
101
  if (!res.ok) throw new Error(data.error || `Error ${res.status}`);
61
102
  return data;
62
103
  }, [endpoint]);
63
104
  const load = React.useCallback(async () => {
105
+ setLoadError(null);
64
106
  try {
65
107
  const d = await call();
66
- setKeys(d.keys ?? []);
108
+ setAccess(d.access === "pending" ? "pending" : "ok");
109
+ setGroups((d.groups ?? []).slice().sort((a, b) => a.name.localeCompare(b.name)));
110
+ setUsers(d.users ?? []);
111
+ setObserved(d.observed ?? []);
67
112
  } catch (e) {
68
- setErr(e instanceof Error ? e.message : "load failed");
69
- setKeys([]);
113
+ setLoadError(e instanceof Error ? e.message : "Could not load groups.");
114
+ setGroups(null);
70
115
  }
71
116
  }, [call]);
72
117
  React.useEffect(() => {
73
118
  load();
74
119
  }, [load]);
120
+ React.useImperativeHandle(ref, () => ({ refresh: load }), [load]);
121
+ const loadDetail = React.useCallback(async (groupId) => {
122
+ setDetailLoading(true);
123
+ setDetail(null);
124
+ setErr(null);
125
+ setAddSel("");
126
+ setNestSel("");
127
+ try {
128
+ setDetail(await call({ action: "group-detail", groupId }));
129
+ } catch (e) {
130
+ setErr(e instanceof Error ? e.message : "Could not load this group.");
131
+ } finally {
132
+ setDetailLoading(false);
133
+ }
134
+ }, [call]);
135
+ function toggle(g) {
136
+ if (open === g.id) {
137
+ setOpen(null);
138
+ setDetail(null);
139
+ return;
140
+ }
141
+ setOpen(g.id);
142
+ loadDetail(g.id);
143
+ }
144
+ async function act(label, body, after) {
145
+ setPending(label);
146
+ setErr(null);
147
+ try {
148
+ await call(body);
149
+ await load();
150
+ if (after) await after();
151
+ } catch (e) {
152
+ setErr(e instanceof Error ? e.message : "The action failed.");
153
+ } finally {
154
+ setPending(null);
155
+ }
156
+ }
157
+ async function createGroup() {
158
+ const name = newName.trim();
159
+ if (!name) return;
160
+ setCreating(true);
161
+ setErr(null);
162
+ try {
163
+ await call({ action: "create-group", name });
164
+ setNewName("");
165
+ setShowCreate(false);
166
+ await load();
167
+ } catch (e) {
168
+ setErr(e instanceof Error ? e.message : "Could not create the group.");
169
+ } finally {
170
+ setCreating(false);
171
+ }
172
+ }
173
+ const vars = {
174
+ // @ts-expect-error CSS custom properties
175
+ "--abz-accent": t.accent,
176
+ background: t.background,
177
+ color: t.text,
178
+ fontFamily: t.fontFamily,
179
+ fontSize: 14
180
+ };
181
+ const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius, overflow: "hidden" };
182
+ const btn = (kind = "ghost", small = false) => ({
183
+ cursor: "pointer",
184
+ borderRadius: 8,
185
+ fontWeight: 600,
186
+ fontSize: small ? 12 : 13,
187
+ padding: small ? "5px 10px" : "8px 14px",
188
+ fontFamily: "inherit",
189
+ border: `1px solid ${kind === "primary" ? t.accent : kind === "danger" ? t.danger : t.border}`,
190
+ background: kind === "primary" ? t.accent : "transparent",
191
+ color: kind === "primary" ? t.accentText : kind === "danger" ? t.danger : t.text,
192
+ whiteSpace: "nowrap"
193
+ });
194
+ const inputStyle = {
195
+ fontFamily: "inherit",
196
+ fontSize: 13,
197
+ padding: "6px 10px",
198
+ borderRadius: 8,
199
+ border: `1px solid ${t.border}`,
200
+ background: t.surface,
201
+ color: t.text,
202
+ minWidth: 0
203
+ };
204
+ const selectStyle = { ...inputStyle, fontWeight: 600, fontSize: 12, cursor: "pointer" };
205
+ const chip = { fontSize: 11, padding: "2px 7px", borderRadius: 999, border: `1px solid ${t.border}`, color: t.muted };
206
+ const busy = pending !== null || creating;
207
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `abz-widget ${className ?? ""}`, style: vars, children: [
208
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `.abz-widget button:focus-visible,.abz-widget select:focus-visible,.abz-widget input:focus-visible{outline:2px solid var(--abz-accent);outline-offset:2px}` }),
209
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: card, children: [
210
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, padding: "14px 16px", borderBottom: `1px solid ${t.border}`, background: headerBg }, children: [
211
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 15 }, children: title }),
212
+ 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
+ ] }),
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" }) })
218
+ ] }),
219
+ 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
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
221
+ "input",
222
+ {
223
+ style: { ...inputStyle, flex: 1 },
224
+ placeholder: "Group name (e.g. admin, reservationists)",
225
+ value: newName,
226
+ onChange: (e) => setNewName(e.target.value),
227
+ onKeyDown: (e) => e.key === "Enter" && createGroup(),
228
+ "aria-label": "New group name"
229
+ }
230
+ ),
231
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("primary", true), disabled: creating || !newName.trim(), onClick: createGroup, children: creating ? "Creating\u2026" : "Create" }),
232
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("ghost", true), onClick: () => {
233
+ setShowCreate(false);
234
+ setNewName("");
235
+ }, children: "Cancel" })
236
+ ] }),
237
+ access === "ok" && groups === null && !loadError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-busy": "true", style: { padding: 24, color: t.muted }, children: "Loading groups\u2026" }),
238
+ access === "ok" && loadError && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "alert", style: { padding: 20, color: t.danger, fontSize: 13 }, children: [
239
+ loadError,
240
+ " ",
241
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: { ...btn("ghost", true), marginLeft: 6 }, onClick: load, children: "Retry" })
242
+ ] }),
243
+ access === "ok" && groups !== null && groups.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: "No groups yet. Create one to start organizing your users." }),
244
+ access === "ok" && groups !== null && groups.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: groups.map((g) => {
245
+ const sealed = g.name === SEALED;
246
+ const isOpen = open === g.id;
247
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: { borderTop: `1px solid ${t.border}` }, children: [
248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
249
+ "button",
250
+ {
251
+ onClick: () => toggle(g),
252
+ "aria-expanded": isOpen,
253
+ style: { display: "flex", width: "100%", gap: 10, alignItems: "center", padding: "12px 16px", background: "transparent", border: "none", cursor: "pointer", color: t.text, fontFamily: "inherit", fontSize: 14, textAlign: "left" },
254
+ children: [
255
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: { color: t.muted, fontSize: 11 }, children: isOpen ? "\u25BE" : "\u25B8" }),
256
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { flex: 1 }, children: g.name }),
257
+ sealed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: chip, children: "admins \xB7 managed by allowlist" }),
258
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { ...chip }, children: [
259
+ g.member_count ?? 0,
260
+ " member",
261
+ (g.member_count ?? 0) === 1 ? "" : "s"
262
+ ] })
263
+ ]
264
+ }
265
+ ),
266
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "0 16px 14px 34px" }, children: [
267
+ detailLoading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: t.muted, fontSize: 13 }, children: "Loading\u2026" }),
268
+ detail && detail.id === g.id && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
269
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: [
270
+ detail.members.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: { display: "flex", gap: 8, alignItems: "center", padding: "6px 0", flexWrap: "wrap" }, children: [
271
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1, minWidth: 140 }, children: userLabel(m) }),
272
+ m.source_label && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: chip, children: m.source_label }),
273
+ sealed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...chip, fontWeight: 600 }, children: "admin" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
274
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
275
+ "select",
276
+ {
277
+ style: selectStyle,
278
+ value: m.role,
279
+ disabled: busy,
280
+ "aria-label": `Role of ${userLabel(m)}`,
281
+ onChange: (e) => act(`role:${m.abz_sub}`, { action: "change-role", groupId: g.id, abzSub: m.abz_sub, role: e.target.value }, () => loadDetail(g.id)),
282
+ children: [
283
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "member", children: "member" }),
284
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "admin", children: "group admin" })
285
+ ]
286
+ }
287
+ ),
288
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
289
+ "button",
290
+ {
291
+ style: btn("danger", true),
292
+ disabled: busy,
293
+ "aria-label": `Remove ${userLabel(m)} from ${g.name}`,
294
+ onClick: () => act(`rm:${m.abz_sub}`, { action: "remove-member", groupId: g.id, abzSub: m.abz_sub }, () => loadDetail(g.id)),
295
+ children: "Remove"
296
+ }
297
+ )
298
+ ] })
299
+ ] }, m.abz_sub)),
300
+ detail.members.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: { color: t.muted, fontSize: 13, padding: "6px 0" }, children: "No members yet." })
301
+ ] }),
302
+ sealed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: t.muted, marginTop: 8 }, children: "Workspace admins are granted by email on the provider\u2019s admin list \u2014 membership can\u2019t be edited here." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
303
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap" }, children: [
304
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
305
+ "select",
306
+ {
307
+ style: { ...selectStyle, flex: 1, minWidth: 160 },
308
+ value: addSel,
309
+ disabled: busy,
310
+ "aria-label": "User to add",
311
+ onChange: (e) => setAddSel(e.target.value),
312
+ children: [
313
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: "Add a user\u2026" }),
314
+ users.filter((u) => !detail.members.some((m) => m.abz_sub === u.abz_sub)).map((u) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: u.abz_sub, children: userLabel(u) }, u.abz_sub))
315
+ ]
316
+ }
317
+ ),
318
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
319
+ "select",
320
+ {
321
+ style: selectStyle,
322
+ value: addRole,
323
+ disabled: busy,
324
+ "aria-label": "Role for the new member",
325
+ onChange: (e) => setAddRole(e.target.value),
326
+ children: [
327
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "member", children: "member" }),
328
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "admin", children: "group admin" })
329
+ ]
330
+ }
331
+ ),
332
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
333
+ "button",
334
+ {
335
+ style: btn("primary", true),
336
+ disabled: busy || !addSel,
337
+ onClick: () => act("add-member", { action: "add-member", groupId: g.id, abzSub: addSel, role: addRole }, () => {
338
+ setAddSel("");
339
+ return loadDetail(g.id);
340
+ }),
341
+ children: "Add"
342
+ }
343
+ )
344
+ ] }),
345
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", marginTop: 12, flexWrap: "wrap" }, children: [
346
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 12, color: t.muted }, children: "Subgroups:" }),
347
+ detail.subgroups.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { ...chip, display: "inline-flex", gap: 6, alignItems: "center" }, children: [
348
+ s.name,
349
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
350
+ "button",
351
+ {
352
+ style: { background: "none", border: "none", cursor: "pointer", color: t.danger, fontSize: 12, padding: 0 },
353
+ disabled: busy,
354
+ "aria-label": `Un-nest ${s.name} from ${g.name}`,
355
+ onClick: () => act("unnest", { action: "remove-subgroup", groupId: g.id, childGroupId: s.id }, () => loadDetail(g.id)),
356
+ children: "\u2715"
357
+ }
358
+ )
359
+ ] }, s.id)),
360
+ detail.subgroups.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 12, color: t.muted }, children: "none" }),
361
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
362
+ "select",
363
+ {
364
+ style: selectStyle,
365
+ value: nestSel,
366
+ disabled: busy,
367
+ "aria-label": "Group to nest",
368
+ onChange: (e) => setNestSel(e.target.value),
369
+ children: [
370
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: "Nest a group\u2026" }),
371
+ groups.filter((x) => x.id !== g.id && x.name !== SEALED && !detail.subgroups.some((s) => s.id === x.id)).map((x) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: x.id, children: x.name }, x.id))
372
+ ]
373
+ }
374
+ ),
375
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
376
+ "button",
377
+ {
378
+ style: btn("ghost", true),
379
+ disabled: busy || !nestSel,
380
+ onClick: () => act("nest", { action: "add-subgroup", groupId: g.id, childGroupId: nestSel }, () => {
381
+ setNestSel("");
382
+ return loadDetail(g.id);
383
+ }),
384
+ children: "Nest"
385
+ }
386
+ )
387
+ ] }),
388
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginTop: 12, display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: confirmDelete === g.id ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
389
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontSize: 12, color: t.text }, children: [
390
+ "Delete \u201C",
391
+ g.name,
392
+ "\u201D? Members lose this group everywhere it\u2019s used. This can\u2019t be undone."
393
+ ] }),
394
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
395
+ "button",
396
+ {
397
+ style: btn("danger", true),
398
+ disabled: busy,
399
+ onClick: () => {
400
+ setConfirmDelete(null);
401
+ act("del", { action: "delete-group", groupId: g.id }, () => {
402
+ setOpen(null);
403
+ setDetail(null);
404
+ });
405
+ },
406
+ children: "Delete group"
407
+ }
408
+ ),
409
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("ghost", true), onClick: () => setConfirmDelete(null), children: "Cancel" })
410
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: { ...btn("ghost", true), color: t.danger }, disabled: busy, onClick: () => setConfirmDelete(g.id), children: "Delete group" }) })
411
+ ] })
412
+ ] })
413
+ ] })
414
+ ] }, g.id);
415
+ }) }),
416
+ access === "ok" && groups !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { borderTop: `1px solid ${t.border}` }, children: [
417
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
418
+ "button",
419
+ {
420
+ onClick: () => setShowTraffic((v) => !v),
421
+ "aria-expanded": showTraffic,
422
+ 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" },
423
+ children: [
424
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", style: { color: t.muted, fontSize: 11 }, children: showTraffic ? "\u25BE" : "\u25B8" }),
425
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { flex: 1 }, children: "Seen in traffic" }),
426
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: chip, children: [
427
+ observed.filter((o) => !o.provisioned).length,
428
+ " new"
429
+ ] })
430
+ ]
431
+ }
432
+ ),
433
+ showTraffic && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { style: { listStyle: "none", margin: 0, padding: "0 16px 14px 34px" }, children: [
434
+ observed.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { style: { color: t.muted, fontSize: 13 }, children: "No identities observed yet \u2014 they appear here after their first API call." }),
435
+ observed.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: { display: "flex", gap: 8, alignItems: "center", padding: "6px 0", flexWrap: "wrap" }, children: [
436
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1, minWidth: 140, fontFamily: t.monoFontFamily, fontSize: 13 }, children: o.email || o.abz_sub }),
437
+ o.source_label && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: chip, children: o.source_label }),
438
+ o.last_seen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontSize: 11, color: t.muted }, children: [
439
+ "seen ",
440
+ relTime(o.last_seen),
441
+ o.seen_count ? ` \xB7 ${o.seen_count}\xD7` : ""
442
+ ] }),
443
+ o.provisioned ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { ...chip, color: t.success, borderColor: t.success }, children: [
444
+ "user",
445
+ o.groups?.length ? ` \xB7 ${o.groups.join(", ")}` : ""
446
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
447
+ "button",
448
+ {
449
+ style: btn("primary", true),
450
+ disabled: busy,
451
+ "aria-label": `Add ${o.email || o.abz_sub} as a user`,
452
+ onClick: () => act("provision", { action: "provision-observed", consumerUserId: o.abz_sub, email: o.email ?? void 0 }),
453
+ children: "Add as user"
454
+ }
455
+ )
456
+ ] }, o.abz_sub))
457
+ ] })
458
+ ] }),
459
+ err && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "alert", style: { padding: "0 16px 14px", fontSize: 12, color: t.danger }, children: err })
460
+ ] })
461
+ ] });
462
+ });
463
+
464
+ // src/react/index.tsx
465
+ var import_jsx_runtime2 = require("react/jsx-runtime");
466
+ var DEFAULTS2 = {
467
+ accent: "#4f46e5",
468
+ accentText: "#ffffff",
469
+ background: "transparent",
470
+ surface: "#ffffff",
471
+ headerBackground: "",
472
+ text: "#111827",
473
+ muted: "#6b7280",
474
+ border: "#e5e7eb",
475
+ danger: "#dc2626",
476
+ success: "#16a34a",
477
+ radius: "12px",
478
+ fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
479
+ monoFontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
480
+ };
481
+ function relTime2(iso) {
482
+ if (!iso) return null;
483
+ const t = new Date(iso).getTime();
484
+ if (Number.isNaN(t)) return null;
485
+ const s = Math.floor((Date.now() - t) / 1e3);
486
+ if (s < 60) return "just now";
487
+ const m = Math.floor(s / 60);
488
+ if (m < 60) return `${m}m ago`;
489
+ const h = Math.floor(m / 60);
490
+ if (h < 24) return `${h}h ago`;
491
+ const d = Math.floor(h / 24);
492
+ if (d < 30) return `${d}d ago`;
493
+ return new Date(iso).toLocaleDateString();
494
+ }
495
+ var ApiKeyWidget = React2.forwardRef(function ApiKeyWidget2({ endpoint = "/api/apiblaze/keys", theme, title = "Your API keys", className }, ref) {
496
+ const t = { ...DEFAULTS2, ...theme ?? {} };
497
+ const headerBg = t.headerBackground || t.surface;
498
+ const [keys, setKeys] = React2.useState(null);
499
+ const [eligible, setEligible] = React2.useState([]);
500
+ const [access, setAccess] = React2.useState("ok");
501
+ const [chosenType, setChosenType] = React2.useState("");
502
+ const [loadError, setLoadError] = React2.useState(null);
503
+ const [secret, setSecret] = React2.useState(null);
504
+ const [creating, setCreating] = React2.useState(false);
505
+ const [pending, setPending] = React2.useState(null);
506
+ const [confirming, setConfirming] = React2.useState(null);
507
+ const [rowError, setRowError] = React2.useState(null);
508
+ const [copied, setCopied] = React2.useState(false);
509
+ const call = React2.useCallback(async (body) => {
510
+ const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
511
+ const data = await res.json().catch(() => ({}));
512
+ if (!res.ok) throw new Error(data.error || `Error ${res.status}`);
513
+ return data;
514
+ }, [endpoint]);
515
+ const load = React2.useCallback(async () => {
516
+ setLoadError(null);
517
+ try {
518
+ const d = await call();
519
+ setKeys((d.keys ?? []).slice().sort(byNewest));
520
+ setEligible(d.keyTypes ?? []);
521
+ setAccess(d.access === "denied" ? "denied" : "ok");
522
+ } catch (e) {
523
+ setLoadError(e instanceof Error ? e.message : "Could not load your keys.");
524
+ setKeys(null);
525
+ }
526
+ }, [call]);
527
+ React2.useEffect(() => {
528
+ load();
529
+ }, [load]);
530
+ React2.useImperativeHandle(ref, () => ({ refresh: load }), [load]);
531
+ React2.useEffect(() => {
532
+ if (eligible.length && !eligible.includes(chosenType)) {
533
+ setChosenType(eligible.includes("call-only") ? "call-only" : eligible[0]);
534
+ }
535
+ }, [eligible, chosenType]);
75
536
  async function create() {
76
- setBusy(true);
537
+ setCreating(true);
538
+ setRowError(null);
77
539
  try {
78
- const d = await call({ action: "create" });
79
- if (d.key) setSecret(d.key);
540
+ const d = await call({ action: "create", keyType: chosenType || void 0 });
541
+ if (d.key) setSecret({ key: d.key, keyId: d.key_id });
80
542
  await load();
81
543
  } catch (e) {
82
- setErr(e instanceof Error ? e.message : "create failed");
544
+ setRowError({ keyId: "", msg: e instanceof Error ? e.message : "Could not create a key." });
83
545
  } finally {
84
- setBusy(false);
546
+ setCreating(false);
85
547
  }
86
548
  }
87
549
  async function reveal(keyId) {
88
- setBusy(true);
550
+ setPending(`reveal:${keyId}`);
551
+ setRowError(null);
89
552
  try {
90
553
  const d = await call({ action: "reveal", keyId });
91
- if (d.key) setSecret(d.key);
92
- else setErr("Key not revealable (expired reveal window).");
554
+ if (d.key) setSecret({ key: d.key, keyId });
555
+ else setRowError({ keyId, msg: "For security, this key can only be shown once \u2014 right after it was created." });
93
556
  } catch (e) {
94
- setErr(e instanceof Error ? e.message : "reveal failed");
557
+ setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not show this key." });
95
558
  } finally {
96
- setBusy(false);
559
+ setPending(null);
97
560
  }
98
561
  }
99
562
  async function rotate(keyId) {
100
- setBusy(true);
563
+ setConfirming(null);
564
+ setPending(`rotate:${keyId}`);
565
+ setRowError(null);
101
566
  try {
102
567
  const d = await call({ action: "rotate", keyId });
103
- if (d.key) setSecret(d.key);
568
+ if (d.key) setSecret({ key: d.key, keyId: d.key_id });
104
569
  await load();
105
570
  } catch (e) {
106
- setErr(e instanceof Error ? e.message : "regenerate failed");
571
+ setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not rotate this key." });
107
572
  } finally {
108
- setBusy(false);
573
+ setPending(null);
109
574
  }
110
575
  }
111
576
  async function revoke(keyId) {
112
- setBusy(true);
577
+ setConfirming(null);
578
+ setPending(`revoke:${keyId}`);
579
+ setRowError(null);
113
580
  try {
114
581
  await call({ action: "revoke", keyId });
115
582
  await load();
116
583
  } catch (e) {
117
- setErr(e instanceof Error ? e.message : "revoke failed");
584
+ setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not revoke this key." });
118
585
  } finally {
119
- setBusy(false);
586
+ setPending(null);
120
587
  }
121
588
  }
122
- function copy(v) {
123
- navigator.clipboard?.writeText(v);
124
- setCopied(true);
125
- setTimeout(() => setCopied(false), 1500);
589
+ async function copy(v) {
590
+ try {
591
+ await navigator.clipboard.writeText(v);
592
+ setCopied(true);
593
+ setTimeout(() => setCopied(false), 1600);
594
+ } catch {
595
+ }
126
596
  }
127
597
  const vars = {
128
- // @ts-expect-error CSS custom props
598
+ // @ts-expect-error CSS custom properties
129
599
  "--abz-accent": t.accent,
130
600
  "--abz-surface": t.surface,
131
601
  "--abz-text": t.text,
132
602
  "--abz-muted": t.muted,
133
603
  "--abz-border": t.border,
604
+ "--abz-danger": t.danger,
134
605
  "--abz-radius": t.radius,
135
606
  background: t.background,
136
607
  color: t.text,
137
- fontFamily: "ui-sans-serif, system-ui, sans-serif",
608
+ fontFamily: t.fontFamily,
138
609
  fontSize: 14
139
610
  };
140
- const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius, padding: 16 };
141
- const btn = (primary2 = false) => ({ cursor: busy ? "default" : "pointer", opacity: busy ? 0.6 : 1, border: `1px solid ${primary2 ? t.accent : t.border}`, background: primary2 ? t.accent : "transparent", color: primary2 ? "#fff" : t.text, borderRadius: 8, padding: "8px 14px", fontWeight: 600, fontSize: 13 });
142
- const mask = (k) => `${k.key_prefix ?? "\u2022\u2022\u2022"}${"\u2022".repeat(8)}${k.key_suffix ?? "\u2022\u2022\u2022"}`;
143
- const primary = (keys ?? []).find((k) => !k.disabled) ?? null;
144
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className, style: vars, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: card, children: [
145
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
146
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 15 }, children: "Your API key" }),
147
- advanced && (keys?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: { ...btn(), padding: "4px 8px", fontSize: 12 }, onClick: () => setOpen((o) => !o), children: open ? "Hide" : "Advanced" })
148
- ] }),
149
- err && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#b91c1c", fontSize: 13, marginBottom: 10 }, children: err }),
150
- secret && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: 12, padding: 12, borderRadius: 8, border: `1px solid ${t.accent}`, background: "rgba(79,70,229,0.06)" }, children: [
151
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 12, color: t.muted, marginBottom: 6 }, children: "Copy it now \u2014 it won\u2019t be shown in full again." }),
152
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
153
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { flex: 1, wordBreak: "break-all", fontSize: 13 }, children: secret }),
154
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn(true), onClick: () => copy(secret), children: copied ? "Copied" : "Copy" })
155
- ] })
156
- ] }),
157
- keys === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: t.muted }, children: "Loading\u2026" }) : primary ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [
158
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { flex: 1, minWidth: 160, color: t.muted }, children: mask(primary) }),
159
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn(), disabled: busy, onClick: () => reveal(primary.key_id), children: "Reveal" }),
160
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn(true), disabled: busy, onClick: () => rotate(primary.key_id), children: "Regenerate" })
161
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn(true), disabled: busy, onClick: create, children: "Generate your API key" }),
162
- advanced && open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: 16, borderTop: `1px solid ${t.border}`, paddingTop: 12 }, children: [
163
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }, children: [
164
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 13, color: t.muted }, children: "All keys" }),
165
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn(), disabled: busy, onClick: create, children: "+ New key" })
611
+ const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius, overflow: "hidden" };
612
+ const btn = (kind = "ghost", small = false) => ({
613
+ cursor: "pointer",
614
+ borderRadius: 8,
615
+ fontWeight: 600,
616
+ fontSize: small ? 12 : 13,
617
+ padding: small ? "5px 10px" : "8px 14px",
618
+ fontFamily: "inherit",
619
+ border: `1px solid ${kind === "primary" ? t.accent : kind === "danger" ? t.danger : t.border}`,
620
+ background: kind === "primary" ? t.accent : "transparent",
621
+ color: kind === "primary" ? t.accentText : kind === "danger" ? t.danger : t.text,
622
+ whiteSpace: "nowrap"
623
+ });
624
+ const selectStyle = {
625
+ fontFamily: "inherit",
626
+ fontSize: 12,
627
+ fontWeight: 600,
628
+ padding: "5px 8px",
629
+ borderRadius: 8,
630
+ border: `1px solid ${t.border}`,
631
+ background: t.surface,
632
+ color: t.text,
633
+ cursor: "pointer"
634
+ };
635
+ const disabledStyle = (on) => on ? { opacity: 0.5, cursor: "not-allowed" } : {};
636
+ const secretBg = `color-mix(in srgb, ${t.accent} 8%, transparent)`;
637
+ const busyAny = creating || pending !== null;
638
+ const denied = access === "denied";
639
+ const showPicker = eligible.length > 1;
640
+ const Picker = showPicker ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("select", { value: chosenType, onChange: (e) => setChosenType(e.target.value), style: selectStyle, "aria-label": "Key type", disabled: busyAny, children: eligible.map((k) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: k, children: k }, k)) }) : null;
641
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `abz-widget ${className ?? ""}`, style: vars, children: [
642
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `.abz-widget button:focus-visible,.abz-widget select:focus-visible{outline:2px solid var(--abz-accent);outline-offset:2px}` }),
643
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: card, children: [
644
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, padding: "14px 16px", borderBottom: `1px solid ${t.border}`, background: headerBg }, children: [
645
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { style: { fontSize: 15 }, children: title }),
646
+ !denied && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
647
+ Picker,
648
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
649
+ "button",
650
+ {
651
+ style: { ...btn("primary", true), ...disabledStyle(busyAny) },
652
+ disabled: busyAny,
653
+ onClick: create,
654
+ "aria-label": "Create a new API key",
655
+ children: creating ? "Creating\u2026" : "+ Create key"
656
+ }
657
+ )
658
+ ] })
166
659
  ] }),
167
- (keys ?? []).length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: t.muted, fontSize: 13 }, children: "No keys yet." }),
168
- (keys ?? []).map((k) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", padding: "8px 0", borderBottom: `1px solid ${t.border}` }, children: [
169
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { flex: 1, fontSize: 12, color: t.muted }, children: mask(k) }),
170
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 11, color: t.muted }, children: k.last_used ? "used" : "unused" }),
171
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: { ...btn(), padding: "4px 8px", fontSize: 12 }, disabled: busy, onClick: () => reveal(k.key_id), children: "Reveal" }),
172
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: { ...btn(), padding: "4px 8px", fontSize: 12, borderColor: "#ef4444", color: "#ef4444" }, disabled: busy, onClick: () => revoke(k.key_id), children: "Revoke" })
173
- ] }, k.key_id))
660
+ denied && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: "Your account doesn\u2019t have API access." }),
661
+ !denied && secret && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { role: "status", "aria-live": "polite", style: { margin: 16, padding: 14, borderRadius: 10, border: `1px solid ${t.accent}`, background: secretBg }, children: [
662
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "New API key" }),
663
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontSize: 12, color: t.muted, marginBottom: 8 }, children: "This is the only time you\u2019ll see this key. Copy it and store it somewhere safe." }),
664
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
665
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
666
+ "input",
667
+ {
668
+ readOnly: true,
669
+ value: secret.key,
670
+ onFocus: (e) => e.currentTarget.select(),
671
+ "aria-label": "New API key value",
672
+ style: { flex: 1, fontFamily: t.monoFontFamily, fontSize: 13, padding: "8px 10px", borderRadius: 8, border: `1px solid ${t.border}`, background: t.surface, color: t.text, minWidth: 0 }
673
+ }
674
+ ),
675
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: { ...btn("primary"), ...copied ? { background: t.success, borderColor: t.success } : {} }, onClick: () => copy(secret.key), children: copied ? "Copied" : "Copy" })
676
+ ] }),
677
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginTop: 10, textAlign: "right" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: btn("ghost", true), onClick: () => setSecret(null), children: "I\u2019ve saved it" }) })
678
+ ] }),
679
+ !denied && keys === null && !loadError && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "aria-busy": "true", style: { padding: 24, color: t.muted }, children: "Loading your keys\u2026" }),
680
+ !denied && loadError && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { role: "alert", style: { padding: 20, color: t.danger, fontSize: 13 }, children: [
681
+ loadError,
682
+ " ",
683
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: { ...btn("ghost", true), marginLeft: 6 }, onClick: load, children: "Retry" })
684
+ ] }),
685
+ !denied && keys !== null && keys.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: "28px 20px", textAlign: "center" }, children: [
686
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { color: t.muted, marginBottom: 14 }, children: "You don\u2019t have an API key yet. Create one to start calling the API." }),
687
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "inline-flex", gap: 8, alignItems: "center" }, children: [
688
+ Picker,
689
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: { ...btn("primary"), ...disabledStyle(creating) }, disabled: creating, onClick: create, children: creating ? "Creating\u2026" : "Create API key" })
690
+ ] })
691
+ ] }),
692
+ !denied && keys !== null && keys.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: keys.map((k) => {
693
+ const anyBusy = busyAny;
694
+ const used = relTime2(k.last_used);
695
+ const created = k.created_at ? new Date(k.created_at).toLocaleDateString() : null;
696
+ const isConfirm = confirming?.keyId === k.key_id;
697
+ const showable = !!k.expires_at;
698
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { style: { padding: "12px 16px", borderTop: `1px solid ${t.border}`, opacity: k.disabled ? 0.6 : 1 }, children: [
699
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }, children: [
700
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("code", { "aria-label": `API key ending in ${k.key_suffix ?? ""}`, style: { flex: 1, minWidth: 150, fontFamily: t.monoFontFamily, color: t.muted }, children: [
701
+ k.key_prefix ?? "\u2022\u2022\u2022",
702
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": "true", children: "\u2022\u2022\u2022\u2022\u2022\u2022" }),
703
+ k.key_suffix ?? "\u2022\u2022\u2022"
704
+ ] }),
705
+ k.environment && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 11, padding: "2px 7px", borderRadius: 999, border: `1px solid ${t.border}`, color: t.muted }, children: k.environment }),
706
+ k.disabled ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 11, fontWeight: 600, color: t.danger }, children: "Revoked" }) : isConfirm ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", flexBasis: "100%", flexWrap: "wrap", marginTop: 6 }, children: [
707
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { fontSize: 12, color: t.text, flex: 1, minWidth: 200 }, children: confirming.action === "rotate" ? "Rotate this key? A new secret is generated now. The current key keeps working for a short grace period, then stops \u2014 anything still using it will break." : "Revoke this key? It stops working immediately and can\u2019t be undone." }),
708
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: btn(confirming.action === "revoke" ? "danger" : "primary", true), onClick: () => confirming.action === "rotate" ? rotate(k.key_id) : revoke(k.key_id), children: confirming.action === "rotate" ? "Rotate key" : "Revoke key" }),
709
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: btn("ghost", true), onClick: () => setConfirming(null), children: "Cancel" })
710
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 8 }, children: [
711
+ showable && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
712
+ "button",
713
+ {
714
+ style: { ...btn("ghost", true), ...disabledStyle(anyBusy) },
715
+ disabled: anyBusy,
716
+ "aria-label": `Show key ending in ${k.key_suffix ?? ""}`,
717
+ onClick: () => reveal(k.key_id),
718
+ children: pending === `reveal:${k.key_id}` ? "Showing\u2026" : "Show"
719
+ }
720
+ ),
721
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
722
+ "button",
723
+ {
724
+ style: { ...btn("ghost", true), ...disabledStyle(anyBusy) },
725
+ disabled: anyBusy,
726
+ "aria-label": `Rotate key ending in ${k.key_suffix ?? ""}`,
727
+ onClick: () => setConfirming({ keyId: k.key_id, action: "rotate" }),
728
+ children: pending === `rotate:${k.key_id}` ? "Rotating\u2026" : "Rotate"
729
+ }
730
+ ),
731
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
732
+ "button",
733
+ {
734
+ style: { ...btn("danger", true), ...disabledStyle(anyBusy) },
735
+ disabled: anyBusy,
736
+ "aria-label": `Revoke key ending in ${k.key_suffix ?? ""}`,
737
+ onClick: () => setConfirming({ keyId: k.key_id, action: "revoke" }),
738
+ children: "Revoke"
739
+ }
740
+ )
741
+ ] })
742
+ ] }),
743
+ (created || used) && !isConfirm && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { fontSize: 11, color: t.muted, marginTop: 5 }, children: [
744
+ created ? `Created ${created}` : "",
745
+ created && used ? " \xB7 " : "",
746
+ used ? `Last used ${used}` : created ? " \xB7 Never used" : "Never used"
747
+ ] }),
748
+ rowError?.keyId === k.key_id && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { role: "alert", style: { fontSize: 12, color: t.danger, marginTop: 6 }, children: rowError.msg })
749
+ ] }, k.key_id);
750
+ }) }),
751
+ !denied && rowError && rowError.keyId === "" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { role: "alert", style: { padding: "0 16px 14px", fontSize: 12, color: t.danger }, children: rowError.msg })
174
752
  ] })
175
- ] }) });
753
+ ] });
754
+ });
755
+ function byNewest(a, b) {
756
+ return new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime();
176
757
  }
177
758
  var react_default = ApiKeyWidget;
178
759
  // Annotate the CommonJS export names for ESM import in node:
179
760
  0 && (module.exports = {
180
- ApiKeyWidget
761
+ ApiKeyWidget,
762
+ UsersGroupsWidget
181
763
  });