apiblaze 0.17.21 → 0.17.22

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_chalk38 = __toESM(require("chalk"));
704
704
 
705
705
  // package.json
706
- var version = "0.17.21";
706
+ var version = "0.17.22";
707
707
 
708
708
  // src/index.ts
709
709
  init_types();
@@ -1,32 +1,59 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  /**
4
- * apiblaze/react — <ApiKeyWidget/>. Drop it into resiresi.com; it talks ONLY to your
5
- * own backend route (which holds the CP key). Shows the logged-in user their API key
6
- * with reveal + regenerate up front and full self-service in an advanced drawer.
4
+ * apiblaze/react — <ApiKeyWidget/>. Drop it into your site (e.g. resiresi.com);
5
+ * it talks ONLY to your own backend route (which holds the CP key). Shows the
6
+ * logged-in user their API keys as a simple list with one always-visible
7
+ * "Create key" action, plus per-key show / rotate / revoke.
7
8
  *
8
9
  * import { ApiKeyWidget } from 'apiblaze/react';
9
10
  * <ApiKeyWidget theme={{ accent: '#e11d48' }} />
10
11
  *
12
+ * Fully white-labelable through `theme` alone — every font and color is a token,
13
+ * so you can match your brand (including a dark "codebox" look) without forking
14
+ * the component. See ApiKeyWidgetTheme.
15
+ *
11
16
  * `endpoint` defaults to /api/apiblaze/keys (where you mounted createApiblazeKeys().handler).
12
17
  */
13
18
 
14
19
  interface ApiKeyWidgetTheme {
20
+ /** Accent / primary action color. */
15
21
  accent?: string;
22
+ /** Text/icon color ON accent buttons. Default #fff. */
23
+ accentText?: string;
24
+ /** Page color behind the card. Default transparent. */
16
25
  background?: string;
26
+ /** Card body color. */
17
27
  surface?: string;
28
+ /** Header strip color. Defaults to `surface` when unset. */
29
+ headerBackground?: string;
30
+ /** Primary text color. */
18
31
  text?: string;
32
+ /** Secondary / muted text color. */
19
33
  muted?: string;
34
+ /** Border + divider color. */
20
35
  border?: string;
36
+ /** Destructive (revoke) color. */
37
+ danger?: string;
38
+ /** Positive (copied) color. */
39
+ success?: string;
40
+ /** Corner radius of the card, e.g. "12px". */
21
41
  radius?: string;
42
+ /** Body font stack. */
43
+ fontFamily?: string;
44
+ /** Monospace font stack for keys and secrets. */
45
+ monoFontFamily?: string;
22
46
  }
23
47
  interface ApiKeyWidgetProps {
24
48
  endpoint?: string;
25
49
  theme?: ApiKeyWidgetTheme;
26
- /** Show the advanced (multi-key) drawer. Default true. */
27
- advanced?: boolean;
50
+ /** Heading text. Default "Your API keys". */
51
+ title?: string;
28
52
  className?: string;
29
53
  }
30
- declare function ApiKeyWidget({ endpoint, theme, advanced, className }: ApiKeyWidgetProps): React.JSX.Element;
54
+ interface ApiKeyWidgetHandle {
55
+ refresh: () => void;
56
+ }
57
+ declare const ApiKeyWidget: React.ForwardRefExoticComponent<ApiKeyWidgetProps & React.RefAttributes<ApiKeyWidgetHandle>>;
31
58
 
32
- export { ApiKeyWidget, type ApiKeyWidgetProps, type ApiKeyWidgetTheme, ApiKeyWidget as default };
59
+ export { ApiKeyWidget, type ApiKeyWidgetHandle, type ApiKeyWidgetProps, type ApiKeyWidgetTheme, ApiKeyWidget as default };
@@ -1,32 +1,59 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  /**
4
- * apiblaze/react — <ApiKeyWidget/>. Drop it into resiresi.com; it talks ONLY to your
5
- * own backend route (which holds the CP key). Shows the logged-in user their API key
6
- * with reveal + regenerate up front and full self-service in an advanced drawer.
4
+ * apiblaze/react — <ApiKeyWidget/>. Drop it into your site (e.g. resiresi.com);
5
+ * it talks ONLY to your own backend route (which holds the CP key). Shows the
6
+ * logged-in user their API keys as a simple list with one always-visible
7
+ * "Create key" action, plus per-key show / rotate / revoke.
7
8
  *
8
9
  * import { ApiKeyWidget } from 'apiblaze/react';
9
10
  * <ApiKeyWidget theme={{ accent: '#e11d48' }} />
10
11
  *
12
+ * Fully white-labelable through `theme` alone — every font and color is a token,
13
+ * so you can match your brand (including a dark "codebox" look) without forking
14
+ * the component. See ApiKeyWidgetTheme.
15
+ *
11
16
  * `endpoint` defaults to /api/apiblaze/keys (where you mounted createApiblazeKeys().handler).
12
17
  */
13
18
 
14
19
  interface ApiKeyWidgetTheme {
20
+ /** Accent / primary action color. */
15
21
  accent?: string;
22
+ /** Text/icon color ON accent buttons. Default #fff. */
23
+ accentText?: string;
24
+ /** Page color behind the card. Default transparent. */
16
25
  background?: string;
26
+ /** Card body color. */
17
27
  surface?: string;
28
+ /** Header strip color. Defaults to `surface` when unset. */
29
+ headerBackground?: string;
30
+ /** Primary text color. */
18
31
  text?: string;
32
+ /** Secondary / muted text color. */
19
33
  muted?: string;
34
+ /** Border + divider color. */
20
35
  border?: string;
36
+ /** Destructive (revoke) color. */
37
+ danger?: string;
38
+ /** Positive (copied) color. */
39
+ success?: string;
40
+ /** Corner radius of the card, e.g. "12px". */
21
41
  radius?: string;
42
+ /** Body font stack. */
43
+ fontFamily?: string;
44
+ /** Monospace font stack for keys and secrets. */
45
+ monoFontFamily?: string;
22
46
  }
23
47
  interface ApiKeyWidgetProps {
24
48
  endpoint?: string;
25
49
  theme?: ApiKeyWidgetTheme;
26
- /** Show the advanced (multi-key) drawer. Default true. */
27
- advanced?: boolean;
50
+ /** Heading text. Default "Your API keys". */
51
+ title?: string;
28
52
  className?: string;
29
53
  }
30
- declare function ApiKeyWidget({ endpoint, theme, advanced, className }: ApiKeyWidgetProps): React.JSX.Element;
54
+ interface ApiKeyWidgetHandle {
55
+ refresh: () => void;
56
+ }
57
+ declare const ApiKeyWidget: React.ForwardRefExoticComponent<ApiKeyWidgetProps & React.RefAttributes<ApiKeyWidgetHandle>>;
31
58
 
32
- export { ApiKeyWidget, type ApiKeyWidgetProps, type ApiKeyWidgetTheme, ApiKeyWidget as default };
59
+ export { ApiKeyWidget, type ApiKeyWidgetHandle, type ApiKeyWidgetProps, type ApiKeyWidgetTheme, ApiKeyWidget as default };
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  "use strict";
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
@@ -38,141 +39,295 @@ var React = __toESM(require("react"));
38
39
  var import_jsx_runtime = require("react/jsx-runtime");
39
40
  var DEFAULTS = {
40
41
  accent: "#4f46e5",
42
+ accentText: "#ffffff",
41
43
  background: "transparent",
42
44
  surface: "#ffffff",
45
+ headerBackground: "",
43
46
  text: "#111827",
44
47
  muted: "#6b7280",
45
48
  border: "#e5e7eb",
46
- radius: "12px"
49
+ danger: "#dc2626",
50
+ success: "#16a34a",
51
+ radius: "12px",
52
+ fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
53
+ monoFontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
47
54
  };
48
- function ApiKeyWidget({ endpoint = "/api/apiblaze/keys", theme, advanced = true, className }) {
55
+ function relTime(iso) {
56
+ if (!iso) return null;
57
+ const t = new Date(iso).getTime();
58
+ if (Number.isNaN(t)) return null;
59
+ const s = Math.floor((Date.now() - t) / 1e3);
60
+ if (s < 60) return "just now";
61
+ const m = Math.floor(s / 60);
62
+ if (m < 60) return `${m}m ago`;
63
+ const h = Math.floor(m / 60);
64
+ if (h < 24) return `${h}h ago`;
65
+ const d = Math.floor(h / 24);
66
+ if (d < 30) return `${d}d ago`;
67
+ return new Date(iso).toLocaleDateString();
68
+ }
69
+ var ApiKeyWidget = React.forwardRef(function ApiKeyWidget2({ endpoint = "/api/apiblaze/keys", theme, title = "Your API keys", className }, ref) {
49
70
  const t = { ...DEFAULTS, ...theme ?? {} };
71
+ const headerBg = t.headerBackground || t.surface;
50
72
  const [keys, setKeys] = React.useState(null);
73
+ const [eligible, setEligible] = React.useState([]);
74
+ const [access, setAccess] = React.useState("ok");
75
+ const [chosenType, setChosenType] = React.useState("");
76
+ const [loadError, setLoadError] = React.useState(null);
51
77
  const [secret, setSecret] = React.useState(null);
52
- const [busy, setBusy] = React.useState(false);
53
- const [err, setErr] = React.useState(null);
54
- const [open, setOpen] = React.useState(false);
78
+ const [creating, setCreating] = React.useState(false);
79
+ const [pending, setPending] = React.useState(null);
80
+ const [confirming, setConfirming] = React.useState(null);
81
+ const [rowError, setRowError] = React.useState(null);
55
82
  const [copied, setCopied] = React.useState(false);
56
83
  const call = React.useCallback(async (body) => {
57
- setErr(null);
58
84
  const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
59
85
  const data = await res.json().catch(() => ({}));
60
86
  if (!res.ok) throw new Error(data.error || `Error ${res.status}`);
61
87
  return data;
62
88
  }, [endpoint]);
63
89
  const load = React.useCallback(async () => {
90
+ setLoadError(null);
64
91
  try {
65
92
  const d = await call();
66
- setKeys(d.keys ?? []);
93
+ setKeys((d.keys ?? []).slice().sort(byNewest));
94
+ setEligible(d.keyTypes ?? []);
95
+ setAccess(d.access === "denied" ? "denied" : "ok");
67
96
  } catch (e) {
68
- setErr(e instanceof Error ? e.message : "load failed");
69
- setKeys([]);
97
+ setLoadError(e instanceof Error ? e.message : "Could not load your keys.");
98
+ setKeys(null);
70
99
  }
71
100
  }, [call]);
72
101
  React.useEffect(() => {
73
102
  load();
74
103
  }, [load]);
104
+ React.useImperativeHandle(ref, () => ({ refresh: load }), [load]);
105
+ React.useEffect(() => {
106
+ if (eligible.length && !eligible.includes(chosenType)) {
107
+ setChosenType(eligible.includes("call-only") ? "call-only" : eligible[0]);
108
+ }
109
+ }, [eligible, chosenType]);
75
110
  async function create() {
76
- setBusy(true);
111
+ setCreating(true);
112
+ setRowError(null);
77
113
  try {
78
- const d = await call({ action: "create" });
79
- if (d.key) setSecret(d.key);
114
+ const d = await call({ action: "create", keyType: chosenType || void 0 });
115
+ if (d.key) setSecret({ key: d.key, keyId: d.key_id });
80
116
  await load();
81
117
  } catch (e) {
82
- setErr(e instanceof Error ? e.message : "create failed");
118
+ setRowError({ keyId: "", msg: e instanceof Error ? e.message : "Could not create a key." });
83
119
  } finally {
84
- setBusy(false);
120
+ setCreating(false);
85
121
  }
86
122
  }
87
123
  async function reveal(keyId) {
88
- setBusy(true);
124
+ setPending(`reveal:${keyId}`);
125
+ setRowError(null);
89
126
  try {
90
127
  const d = await call({ action: "reveal", keyId });
91
- if (d.key) setSecret(d.key);
92
- else setErr("Key not revealable (expired reveal window).");
128
+ if (d.key) setSecret({ key: d.key, keyId });
129
+ else setRowError({ keyId, msg: "For security, this key can only be shown once \u2014 right after it was created." });
93
130
  } catch (e) {
94
- setErr(e instanceof Error ? e.message : "reveal failed");
131
+ setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not show this key." });
95
132
  } finally {
96
- setBusy(false);
133
+ setPending(null);
97
134
  }
98
135
  }
99
136
  async function rotate(keyId) {
100
- setBusy(true);
137
+ setConfirming(null);
138
+ setPending(`rotate:${keyId}`);
139
+ setRowError(null);
101
140
  try {
102
141
  const d = await call({ action: "rotate", keyId });
103
- if (d.key) setSecret(d.key);
142
+ if (d.key) setSecret({ key: d.key, keyId: d.key_id });
104
143
  await load();
105
144
  } catch (e) {
106
- setErr(e instanceof Error ? e.message : "regenerate failed");
145
+ setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not rotate this key." });
107
146
  } finally {
108
- setBusy(false);
147
+ setPending(null);
109
148
  }
110
149
  }
111
150
  async function revoke(keyId) {
112
- setBusy(true);
151
+ setConfirming(null);
152
+ setPending(`revoke:${keyId}`);
153
+ setRowError(null);
113
154
  try {
114
155
  await call({ action: "revoke", keyId });
115
156
  await load();
116
157
  } catch (e) {
117
- setErr(e instanceof Error ? e.message : "revoke failed");
158
+ setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not revoke this key." });
118
159
  } finally {
119
- setBusy(false);
160
+ setPending(null);
120
161
  }
121
162
  }
122
- function copy(v) {
123
- navigator.clipboard?.writeText(v);
124
- setCopied(true);
125
- setTimeout(() => setCopied(false), 1500);
163
+ async function copy(v) {
164
+ try {
165
+ await navigator.clipboard.writeText(v);
166
+ setCopied(true);
167
+ setTimeout(() => setCopied(false), 1600);
168
+ } catch {
169
+ }
126
170
  }
127
171
  const vars = {
128
- // @ts-expect-error CSS custom props
172
+ // @ts-expect-error CSS custom properties
129
173
  "--abz-accent": t.accent,
130
174
  "--abz-surface": t.surface,
131
175
  "--abz-text": t.text,
132
176
  "--abz-muted": t.muted,
133
177
  "--abz-border": t.border,
178
+ "--abz-danger": t.danger,
134
179
  "--abz-radius": t.radius,
135
180
  background: t.background,
136
181
  color: t.text,
137
- fontFamily: "ui-sans-serif, system-ui, sans-serif",
182
+ fontFamily: t.fontFamily,
138
183
  fontSize: 14
139
184
  };
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" })
185
+ const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius, overflow: "hidden" };
186
+ const btn = (kind = "ghost", small = false) => ({
187
+ cursor: "pointer",
188
+ borderRadius: 8,
189
+ fontWeight: 600,
190
+ fontSize: small ? 12 : 13,
191
+ padding: small ? "5px 10px" : "8px 14px",
192
+ fontFamily: "inherit",
193
+ border: `1px solid ${kind === "primary" ? t.accent : kind === "danger" ? t.danger : t.border}`,
194
+ background: kind === "primary" ? t.accent : "transparent",
195
+ color: kind === "primary" ? t.accentText : kind === "danger" ? t.danger : t.text,
196
+ whiteSpace: "nowrap"
197
+ });
198
+ const selectStyle = {
199
+ fontFamily: "inherit",
200
+ fontSize: 12,
201
+ fontWeight: 600,
202
+ padding: "5px 8px",
203
+ borderRadius: 8,
204
+ border: `1px solid ${t.border}`,
205
+ background: t.surface,
206
+ color: t.text,
207
+ cursor: "pointer"
208
+ };
209
+ const disabledStyle = (on) => on ? { opacity: 0.5, cursor: "not-allowed" } : {};
210
+ const secretBg = `color-mix(in srgb, ${t.accent} 8%, transparent)`;
211
+ const busyAny = creating || pending !== null;
212
+ const denied = access === "denied";
213
+ const showPicker = eligible.length > 1;
214
+ const Picker = showPicker ? /* @__PURE__ */ (0, import_jsx_runtime.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_runtime.jsx)("option", { value: k, children: k }, k)) }) : null;
215
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `abz-widget ${className ?? ""}`, style: vars, children: [
216
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `.abz-widget button:focus-visible,.abz-widget select:focus-visible{outline:2px solid var(--abz-accent);outline-offset:2px}` }),
217
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: card, children: [
218
+ /* @__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: [
219
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 15 }, children: title }),
220
+ !denied && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
221
+ Picker,
222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
223
+ "button",
224
+ {
225
+ style: { ...btn("primary", true), ...disabledStyle(busyAny) },
226
+ disabled: busyAny,
227
+ onClick: create,
228
+ "aria-label": "Create a new API key",
229
+ children: creating ? "Creating\u2026" : "+ Create key"
230
+ }
231
+ )
232
+ ] })
233
+ ] }),
234
+ denied && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: "Your account doesn\u2019t have API access." }),
235
+ !denied && secret && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "status", "aria-live": "polite", style: { margin: 16, padding: 14, borderRadius: 10, border: `1px solid ${t.accent}`, background: secretBg }, children: [
236
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "New API key" }),
237
+ /* @__PURE__ */ (0, import_jsx_runtime.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." }),
238
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
239
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
240
+ "input",
241
+ {
242
+ readOnly: true,
243
+ value: secret.key,
244
+ onFocus: (e) => e.currentTarget.select(),
245
+ "aria-label": "New API key value",
246
+ 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 }
247
+ }
248
+ ),
249
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: { ...btn("primary"), ...copied ? { background: t.success, borderColor: t.success } : {} }, onClick: () => copy(secret.key), children: copied ? "Copied" : "Copy" })
250
+ ] }),
251
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginTop: 10, textAlign: "right" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("ghost", true), onClick: () => setSecret(null), children: "I\u2019ve saved it" }) })
252
+ ] }),
253
+ !denied && keys === null && !loadError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "aria-busy": "true", style: { padding: 24, color: t.muted }, children: "Loading your keys\u2026" }),
254
+ !denied && loadError && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { role: "alert", style: { padding: 20, color: t.danger, fontSize: 13 }, children: [
255
+ loadError,
256
+ " ",
257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: { ...btn("ghost", true), marginLeft: 6 }, onClick: load, children: "Retry" })
166
258
  ] }),
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))
259
+ !denied && keys !== null && keys.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "28px 20px", textAlign: "center" }, children: [
260
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: t.muted, marginBottom: 14 }, children: "You don\u2019t have an API key yet. Create one to start calling the API." }),
261
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "inline-flex", gap: 8, alignItems: "center" }, children: [
262
+ Picker,
263
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: { ...btn("primary"), ...disabledStyle(creating) }, disabled: creating, onClick: create, children: creating ? "Creating\u2026" : "Create API key" })
264
+ ] })
265
+ ] }),
266
+ !denied && keys !== null && keys.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: keys.map((k) => {
267
+ const anyBusy = busyAny;
268
+ const used = relTime(k.last_used);
269
+ const created = k.created_at ? new Date(k.created_at).toLocaleDateString() : null;
270
+ const isConfirm = confirming?.keyId === k.key_id;
271
+ const showable = !!k.expires_at;
272
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: { padding: "12px 16px", borderTop: `1px solid ${t.border}`, opacity: k.disabled ? 0.6 : 1 }, children: [
273
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }, children: [
274
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("code", { "aria-label": `API key ending in ${k.key_suffix ?? ""}`, style: { flex: 1, minWidth: 150, fontFamily: t.monoFontFamily, color: t.muted }, children: [
275
+ k.key_prefix ?? "\u2022\u2022\u2022",
276
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u2022\u2022\u2022\u2022\u2022\u2022" }),
277
+ k.key_suffix ?? "\u2022\u2022\u2022"
278
+ ] }),
279
+ k.environment && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 11, padding: "2px 7px", borderRadius: 999, border: `1px solid ${t.border}`, color: t.muted }, children: k.environment }),
280
+ k.disabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 11, fontWeight: 600, color: t.danger }, children: "Revoked" }) : isConfirm ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", flexBasis: "100%", flexWrap: "wrap", marginTop: 6 }, children: [
281
+ /* @__PURE__ */ (0, import_jsx_runtime.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." }),
282
+ /* @__PURE__ */ (0, import_jsx_runtime.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" }),
283
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: btn("ghost", true), onClick: () => setConfirming(null), children: "Cancel" })
284
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8 }, children: [
285
+ showable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
286
+ "button",
287
+ {
288
+ style: { ...btn("ghost", true), ...disabledStyle(anyBusy) },
289
+ disabled: anyBusy,
290
+ "aria-label": `Show key ending in ${k.key_suffix ?? ""}`,
291
+ onClick: () => reveal(k.key_id),
292
+ children: pending === `reveal:${k.key_id}` ? "Showing\u2026" : "Show"
293
+ }
294
+ ),
295
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
296
+ "button",
297
+ {
298
+ style: { ...btn("ghost", true), ...disabledStyle(anyBusy) },
299
+ disabled: anyBusy,
300
+ "aria-label": `Rotate key ending in ${k.key_suffix ?? ""}`,
301
+ onClick: () => setConfirming({ keyId: k.key_id, action: "rotate" }),
302
+ children: pending === `rotate:${k.key_id}` ? "Rotating\u2026" : "Rotate"
303
+ }
304
+ ),
305
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
306
+ "button",
307
+ {
308
+ style: { ...btn("danger", true), ...disabledStyle(anyBusy) },
309
+ disabled: anyBusy,
310
+ "aria-label": `Revoke key ending in ${k.key_suffix ?? ""}`,
311
+ onClick: () => setConfirming({ keyId: k.key_id, action: "revoke" }),
312
+ children: "Revoke"
313
+ }
314
+ )
315
+ ] })
316
+ ] }),
317
+ (created || used) && !isConfirm && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 11, color: t.muted, marginTop: 5 }, children: [
318
+ created ? `Created ${created}` : "",
319
+ created && used ? " \xB7 " : "",
320
+ used ? `Last used ${used}` : created ? " \xB7 Never used" : "Never used"
321
+ ] }),
322
+ rowError?.keyId === k.key_id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "alert", style: { fontSize: 12, color: t.danger, marginTop: 6 }, children: rowError.msg })
323
+ ] }, k.key_id);
324
+ }) }),
325
+ !denied && rowError && rowError.keyId === "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "alert", style: { padding: "0 16px 14px", fontSize: 12, color: t.danger }, children: rowError.msg })
174
326
  ] })
175
- ] }) });
327
+ ] });
328
+ });
329
+ function byNewest(a, b) {
330
+ return new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime();
176
331
  }
177
332
  var react_default = ApiKeyWidget;
178
333
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,143 +1,299 @@
1
+ "use client";
2
+
1
3
  // src/react/index.tsx
2
4
  import * as React from "react";
3
5
  import { jsx, jsxs } from "react/jsx-runtime";
4
6
  var DEFAULTS = {
5
7
  accent: "#4f46e5",
8
+ accentText: "#ffffff",
6
9
  background: "transparent",
7
10
  surface: "#ffffff",
11
+ headerBackground: "",
8
12
  text: "#111827",
9
13
  muted: "#6b7280",
10
14
  border: "#e5e7eb",
11
- radius: "12px"
15
+ danger: "#dc2626",
16
+ success: "#16a34a",
17
+ radius: "12px",
18
+ fontFamily: "ui-sans-serif, system-ui, -apple-system, sans-serif",
19
+ monoFontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
12
20
  };
13
- function ApiKeyWidget({ endpoint = "/api/apiblaze/keys", theme, advanced = true, className }) {
21
+ function relTime(iso) {
22
+ if (!iso) return null;
23
+ const t = new Date(iso).getTime();
24
+ if (Number.isNaN(t)) return null;
25
+ const s = Math.floor((Date.now() - t) / 1e3);
26
+ if (s < 60) return "just now";
27
+ const m = Math.floor(s / 60);
28
+ if (m < 60) return `${m}m ago`;
29
+ const h = Math.floor(m / 60);
30
+ if (h < 24) return `${h}h ago`;
31
+ const d = Math.floor(h / 24);
32
+ if (d < 30) return `${d}d ago`;
33
+ return new Date(iso).toLocaleDateString();
34
+ }
35
+ var ApiKeyWidget = React.forwardRef(function ApiKeyWidget2({ endpoint = "/api/apiblaze/keys", theme, title = "Your API keys", className }, ref) {
14
36
  const t = { ...DEFAULTS, ...theme ?? {} };
37
+ const headerBg = t.headerBackground || t.surface;
15
38
  const [keys, setKeys] = React.useState(null);
39
+ const [eligible, setEligible] = React.useState([]);
40
+ const [access, setAccess] = React.useState("ok");
41
+ const [chosenType, setChosenType] = React.useState("");
42
+ const [loadError, setLoadError] = React.useState(null);
16
43
  const [secret, setSecret] = React.useState(null);
17
- const [busy, setBusy] = React.useState(false);
18
- const [err, setErr] = React.useState(null);
19
- const [open, setOpen] = React.useState(false);
44
+ const [creating, setCreating] = React.useState(false);
45
+ const [pending, setPending] = React.useState(null);
46
+ const [confirming, setConfirming] = React.useState(null);
47
+ const [rowError, setRowError] = React.useState(null);
20
48
  const [copied, setCopied] = React.useState(false);
21
49
  const call = React.useCallback(async (body) => {
22
- setErr(null);
23
50
  const res = await fetch(endpoint, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : { cache: "no-store" });
24
51
  const data = await res.json().catch(() => ({}));
25
52
  if (!res.ok) throw new Error(data.error || `Error ${res.status}`);
26
53
  return data;
27
54
  }, [endpoint]);
28
55
  const load = React.useCallback(async () => {
56
+ setLoadError(null);
29
57
  try {
30
58
  const d = await call();
31
- setKeys(d.keys ?? []);
59
+ setKeys((d.keys ?? []).slice().sort(byNewest));
60
+ setEligible(d.keyTypes ?? []);
61
+ setAccess(d.access === "denied" ? "denied" : "ok");
32
62
  } catch (e) {
33
- setErr(e instanceof Error ? e.message : "load failed");
34
- setKeys([]);
63
+ setLoadError(e instanceof Error ? e.message : "Could not load your keys.");
64
+ setKeys(null);
35
65
  }
36
66
  }, [call]);
37
67
  React.useEffect(() => {
38
68
  load();
39
69
  }, [load]);
70
+ React.useImperativeHandle(ref, () => ({ refresh: load }), [load]);
71
+ React.useEffect(() => {
72
+ if (eligible.length && !eligible.includes(chosenType)) {
73
+ setChosenType(eligible.includes("call-only") ? "call-only" : eligible[0]);
74
+ }
75
+ }, [eligible, chosenType]);
40
76
  async function create() {
41
- setBusy(true);
77
+ setCreating(true);
78
+ setRowError(null);
42
79
  try {
43
- const d = await call({ action: "create" });
44
- if (d.key) setSecret(d.key);
80
+ const d = await call({ action: "create", keyType: chosenType || void 0 });
81
+ if (d.key) setSecret({ key: d.key, keyId: d.key_id });
45
82
  await load();
46
83
  } catch (e) {
47
- setErr(e instanceof Error ? e.message : "create failed");
84
+ setRowError({ keyId: "", msg: e instanceof Error ? e.message : "Could not create a key." });
48
85
  } finally {
49
- setBusy(false);
86
+ setCreating(false);
50
87
  }
51
88
  }
52
89
  async function reveal(keyId) {
53
- setBusy(true);
90
+ setPending(`reveal:${keyId}`);
91
+ setRowError(null);
54
92
  try {
55
93
  const d = await call({ action: "reveal", keyId });
56
- if (d.key) setSecret(d.key);
57
- else setErr("Key not revealable (expired reveal window).");
94
+ if (d.key) setSecret({ key: d.key, keyId });
95
+ else setRowError({ keyId, msg: "For security, this key can only be shown once \u2014 right after it was created." });
58
96
  } catch (e) {
59
- setErr(e instanceof Error ? e.message : "reveal failed");
97
+ setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not show this key." });
60
98
  } finally {
61
- setBusy(false);
99
+ setPending(null);
62
100
  }
63
101
  }
64
102
  async function rotate(keyId) {
65
- setBusy(true);
103
+ setConfirming(null);
104
+ setPending(`rotate:${keyId}`);
105
+ setRowError(null);
66
106
  try {
67
107
  const d = await call({ action: "rotate", keyId });
68
- if (d.key) setSecret(d.key);
108
+ if (d.key) setSecret({ key: d.key, keyId: d.key_id });
69
109
  await load();
70
110
  } catch (e) {
71
- setErr(e instanceof Error ? e.message : "regenerate failed");
111
+ setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not rotate this key." });
72
112
  } finally {
73
- setBusy(false);
113
+ setPending(null);
74
114
  }
75
115
  }
76
116
  async function revoke(keyId) {
77
- setBusy(true);
117
+ setConfirming(null);
118
+ setPending(`revoke:${keyId}`);
119
+ setRowError(null);
78
120
  try {
79
121
  await call({ action: "revoke", keyId });
80
122
  await load();
81
123
  } catch (e) {
82
- setErr(e instanceof Error ? e.message : "revoke failed");
124
+ setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not revoke this key." });
83
125
  } finally {
84
- setBusy(false);
126
+ setPending(null);
85
127
  }
86
128
  }
87
- function copy(v) {
88
- navigator.clipboard?.writeText(v);
89
- setCopied(true);
90
- setTimeout(() => setCopied(false), 1500);
129
+ async function copy(v) {
130
+ try {
131
+ await navigator.clipboard.writeText(v);
132
+ setCopied(true);
133
+ setTimeout(() => setCopied(false), 1600);
134
+ } catch {
135
+ }
91
136
  }
92
137
  const vars = {
93
- // @ts-expect-error CSS custom props
138
+ // @ts-expect-error CSS custom properties
94
139
  "--abz-accent": t.accent,
95
140
  "--abz-surface": t.surface,
96
141
  "--abz-text": t.text,
97
142
  "--abz-muted": t.muted,
98
143
  "--abz-border": t.border,
144
+ "--abz-danger": t.danger,
99
145
  "--abz-radius": t.radius,
100
146
  background: t.background,
101
147
  color: t.text,
102
- fontFamily: "ui-sans-serif, system-ui, sans-serif",
148
+ fontFamily: t.fontFamily,
103
149
  fontSize: 14
104
150
  };
105
- const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius, padding: 16 };
106
- 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 });
107
- const mask = (k) => `${k.key_prefix ?? "\u2022\u2022\u2022"}${"\u2022".repeat(8)}${k.key_suffix ?? "\u2022\u2022\u2022"}`;
108
- const primary = (keys ?? []).find((k) => !k.disabled) ?? null;
109
- return /* @__PURE__ */ jsx("div", { className, style: vars, children: /* @__PURE__ */ jsxs("div", { style: card, children: [
110
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
111
- /* @__PURE__ */ jsx("strong", { style: { fontSize: 15 }, children: "Your API key" }),
112
- advanced && (keys?.length ?? 0) > 0 && /* @__PURE__ */ jsx("button", { style: { ...btn(), padding: "4px 8px", fontSize: 12 }, onClick: () => setOpen((o) => !o), children: open ? "Hide" : "Advanced" })
113
- ] }),
114
- err && /* @__PURE__ */ jsx("div", { style: { color: "#b91c1c", fontSize: 13, marginBottom: 10 }, children: err }),
115
- secret && /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12, padding: 12, borderRadius: 8, border: `1px solid ${t.accent}`, background: "rgba(79,70,229,0.06)" }, children: [
116
- /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: t.muted, marginBottom: 6 }, children: "Copy it now \u2014 it won\u2019t be shown in full again." }),
117
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
118
- /* @__PURE__ */ jsx("code", { style: { flex: 1, wordBreak: "break-all", fontSize: 13 }, children: secret }),
119
- /* @__PURE__ */ jsx("button", { style: btn(true), onClick: () => copy(secret), children: copied ? "Copied" : "Copy" })
120
- ] })
121
- ] }),
122
- keys === null ? /* @__PURE__ */ jsx("div", { style: { color: t.muted }, children: "Loading\u2026" }) : primary ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [
123
- /* @__PURE__ */ jsx("code", { style: { flex: 1, minWidth: 160, color: t.muted }, children: mask(primary) }),
124
- /* @__PURE__ */ jsx("button", { style: btn(), disabled: busy, onClick: () => reveal(primary.key_id), children: "Reveal" }),
125
- /* @__PURE__ */ jsx("button", { style: btn(true), disabled: busy, onClick: () => rotate(primary.key_id), children: "Regenerate" })
126
- ] }) : /* @__PURE__ */ jsx("button", { style: btn(true), disabled: busy, onClick: create, children: "Generate your API key" }),
127
- advanced && open && /* @__PURE__ */ jsxs("div", { style: { marginTop: 16, borderTop: `1px solid ${t.border}`, paddingTop: 12 }, children: [
128
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }, children: [
129
- /* @__PURE__ */ jsx("span", { style: { fontSize: 13, color: t.muted }, children: "All keys" }),
130
- /* @__PURE__ */ jsx("button", { style: btn(), disabled: busy, onClick: create, children: "+ New key" })
151
+ const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius, overflow: "hidden" };
152
+ const btn = (kind = "ghost", small = false) => ({
153
+ cursor: "pointer",
154
+ borderRadius: 8,
155
+ fontWeight: 600,
156
+ fontSize: small ? 12 : 13,
157
+ padding: small ? "5px 10px" : "8px 14px",
158
+ fontFamily: "inherit",
159
+ border: `1px solid ${kind === "primary" ? t.accent : kind === "danger" ? t.danger : t.border}`,
160
+ background: kind === "primary" ? t.accent : "transparent",
161
+ color: kind === "primary" ? t.accentText : kind === "danger" ? t.danger : t.text,
162
+ whiteSpace: "nowrap"
163
+ });
164
+ const selectStyle = {
165
+ fontFamily: "inherit",
166
+ fontSize: 12,
167
+ fontWeight: 600,
168
+ padding: "5px 8px",
169
+ borderRadius: 8,
170
+ border: `1px solid ${t.border}`,
171
+ background: t.surface,
172
+ color: t.text,
173
+ cursor: "pointer"
174
+ };
175
+ const disabledStyle = (on) => on ? { opacity: 0.5, cursor: "not-allowed" } : {};
176
+ const secretBg = `color-mix(in srgb, ${t.accent} 8%, transparent)`;
177
+ const busyAny = creating || pending !== null;
178
+ const denied = access === "denied";
179
+ const showPicker = eligible.length > 1;
180
+ const Picker = showPicker ? /* @__PURE__ */ jsx("select", { value: chosenType, onChange: (e) => setChosenType(e.target.value), style: selectStyle, "aria-label": "Key type", disabled: busyAny, children: eligible.map((k) => /* @__PURE__ */ jsx("option", { value: k, children: k }, k)) }) : null;
181
+ return /* @__PURE__ */ jsxs("div", { className: `abz-widget ${className ?? ""}`, style: vars, children: [
182
+ /* @__PURE__ */ jsx("style", { children: `.abz-widget button:focus-visible,.abz-widget select:focus-visible{outline:2px solid var(--abz-accent);outline-offset:2px}` }),
183
+ /* @__PURE__ */ jsxs("div", { style: card, children: [
184
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12, padding: "14px 16px", borderBottom: `1px solid ${t.border}`, background: headerBg }, children: [
185
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: 15 }, children: title }),
186
+ !denied && /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
187
+ Picker,
188
+ /* @__PURE__ */ jsx(
189
+ "button",
190
+ {
191
+ style: { ...btn("primary", true), ...disabledStyle(busyAny) },
192
+ disabled: busyAny,
193
+ onClick: create,
194
+ "aria-label": "Create a new API key",
195
+ children: creating ? "Creating\u2026" : "+ Create key"
196
+ }
197
+ )
198
+ ] })
199
+ ] }),
200
+ denied && /* @__PURE__ */ jsx("div", { style: { padding: "28px 20px", textAlign: "center", color: t.muted }, children: "Your account doesn\u2019t have API access." }),
201
+ !denied && secret && /* @__PURE__ */ jsxs("div", { role: "status", "aria-live": "polite", style: { margin: 16, padding: 14, borderRadius: 10, border: `1px solid ${t.accent}`, background: secretBg }, children: [
202
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "New API key" }),
203
+ /* @__PURE__ */ 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." }),
204
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
205
+ /* @__PURE__ */ jsx(
206
+ "input",
207
+ {
208
+ readOnly: true,
209
+ value: secret.key,
210
+ onFocus: (e) => e.currentTarget.select(),
211
+ "aria-label": "New API key value",
212
+ 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 }
213
+ }
214
+ ),
215
+ /* @__PURE__ */ jsx("button", { style: { ...btn("primary"), ...copied ? { background: t.success, borderColor: t.success } : {} }, onClick: () => copy(secret.key), children: copied ? "Copied" : "Copy" })
216
+ ] }),
217
+ /* @__PURE__ */ jsx("div", { style: { marginTop: 10, textAlign: "right" }, children: /* @__PURE__ */ jsx("button", { style: btn("ghost", true), onClick: () => setSecret(null), children: "I\u2019ve saved it" }) })
218
+ ] }),
219
+ !denied && keys === null && !loadError && /* @__PURE__ */ jsx("div", { "aria-busy": "true", style: { padding: 24, color: t.muted }, children: "Loading your keys\u2026" }),
220
+ !denied && loadError && /* @__PURE__ */ jsxs("div", { role: "alert", style: { padding: 20, color: t.danger, fontSize: 13 }, children: [
221
+ loadError,
222
+ " ",
223
+ /* @__PURE__ */ jsx("button", { style: { ...btn("ghost", true), marginLeft: 6 }, onClick: load, children: "Retry" })
224
+ ] }),
225
+ !denied && keys !== null && keys.length === 0 && /* @__PURE__ */ jsxs("div", { style: { padding: "28px 20px", textAlign: "center" }, children: [
226
+ /* @__PURE__ */ jsx("div", { style: { color: t.muted, marginBottom: 14 }, children: "You don\u2019t have an API key yet. Create one to start calling the API." }),
227
+ /* @__PURE__ */ jsxs("div", { style: { display: "inline-flex", gap: 8, alignItems: "center" }, children: [
228
+ Picker,
229
+ /* @__PURE__ */ jsx("button", { style: { ...btn("primary"), ...disabledStyle(creating) }, disabled: creating, onClick: create, children: creating ? "Creating\u2026" : "Create API key" })
230
+ ] })
131
231
  ] }),
132
- (keys ?? []).length === 0 && /* @__PURE__ */ jsx("div", { style: { color: t.muted, fontSize: 13 }, children: "No keys yet." }),
133
- (keys ?? []).map((k) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", padding: "8px 0", borderBottom: `1px solid ${t.border}` }, children: [
134
- /* @__PURE__ */ jsx("code", { style: { flex: 1, fontSize: 12, color: t.muted }, children: mask(k) }),
135
- /* @__PURE__ */ jsx("span", { style: { fontSize: 11, color: t.muted }, children: k.last_used ? "used" : "unused" }),
136
- /* @__PURE__ */ jsx("button", { style: { ...btn(), padding: "4px 8px", fontSize: 12 }, disabled: busy, onClick: () => reveal(k.key_id), children: "Reveal" }),
137
- /* @__PURE__ */ jsx("button", { style: { ...btn(), padding: "4px 8px", fontSize: 12, borderColor: "#ef4444", color: "#ef4444" }, disabled: busy, onClick: () => revoke(k.key_id), children: "Revoke" })
138
- ] }, k.key_id))
232
+ !denied && keys !== null && keys.length > 0 && /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: keys.map((k) => {
233
+ const anyBusy = busyAny;
234
+ const used = relTime(k.last_used);
235
+ const created = k.created_at ? new Date(k.created_at).toLocaleDateString() : null;
236
+ const isConfirm = confirming?.keyId === k.key_id;
237
+ const showable = !!k.expires_at;
238
+ return /* @__PURE__ */ jsxs("li", { style: { padding: "12px 16px", borderTop: `1px solid ${t.border}`, opacity: k.disabled ? 0.6 : 1 }, children: [
239
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }, children: [
240
+ /* @__PURE__ */ jsxs("code", { "aria-label": `API key ending in ${k.key_suffix ?? ""}`, style: { flex: 1, minWidth: 150, fontFamily: t.monoFontFamily, color: t.muted }, children: [
241
+ k.key_prefix ?? "\u2022\u2022\u2022",
242
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2022\u2022\u2022\u2022\u2022\u2022" }),
243
+ k.key_suffix ?? "\u2022\u2022\u2022"
244
+ ] }),
245
+ k.environment && /* @__PURE__ */ jsx("span", { style: { fontSize: 11, padding: "2px 7px", borderRadius: 999, border: `1px solid ${t.border}`, color: t.muted }, children: k.environment }),
246
+ k.disabled ? /* @__PURE__ */ jsx("span", { style: { fontSize: 11, fontWeight: 600, color: t.danger }, children: "Revoked" }) : isConfirm ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flexBasis: "100%", flexWrap: "wrap", marginTop: 6 }, children: [
247
+ /* @__PURE__ */ 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." }),
248
+ /* @__PURE__ */ 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" }),
249
+ /* @__PURE__ */ jsx("button", { style: btn("ghost", true), onClick: () => setConfirming(null), children: "Cancel" })
250
+ ] }) : /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
251
+ showable && /* @__PURE__ */ jsx(
252
+ "button",
253
+ {
254
+ style: { ...btn("ghost", true), ...disabledStyle(anyBusy) },
255
+ disabled: anyBusy,
256
+ "aria-label": `Show key ending in ${k.key_suffix ?? ""}`,
257
+ onClick: () => reveal(k.key_id),
258
+ children: pending === `reveal:${k.key_id}` ? "Showing\u2026" : "Show"
259
+ }
260
+ ),
261
+ /* @__PURE__ */ jsx(
262
+ "button",
263
+ {
264
+ style: { ...btn("ghost", true), ...disabledStyle(anyBusy) },
265
+ disabled: anyBusy,
266
+ "aria-label": `Rotate key ending in ${k.key_suffix ?? ""}`,
267
+ onClick: () => setConfirming({ keyId: k.key_id, action: "rotate" }),
268
+ children: pending === `rotate:${k.key_id}` ? "Rotating\u2026" : "Rotate"
269
+ }
270
+ ),
271
+ /* @__PURE__ */ jsx(
272
+ "button",
273
+ {
274
+ style: { ...btn("danger", true), ...disabledStyle(anyBusy) },
275
+ disabled: anyBusy,
276
+ "aria-label": `Revoke key ending in ${k.key_suffix ?? ""}`,
277
+ onClick: () => setConfirming({ keyId: k.key_id, action: "revoke" }),
278
+ children: "Revoke"
279
+ }
280
+ )
281
+ ] })
282
+ ] }),
283
+ (created || used) && !isConfirm && /* @__PURE__ */ jsxs("div", { style: { fontSize: 11, color: t.muted, marginTop: 5 }, children: [
284
+ created ? `Created ${created}` : "",
285
+ created && used ? " \xB7 " : "",
286
+ used ? `Last used ${used}` : created ? " \xB7 Never used" : "Never used"
287
+ ] }),
288
+ rowError?.keyId === k.key_id && /* @__PURE__ */ jsx("div", { role: "alert", style: { fontSize: 12, color: t.danger, marginTop: 6 }, children: rowError.msg })
289
+ ] }, k.key_id);
290
+ }) }),
291
+ !denied && rowError && rowError.keyId === "" && /* @__PURE__ */ jsx("div", { role: "alert", style: { padding: "0 16px 14px", fontSize: 12, color: t.danger }, children: rowError.msg })
139
292
  ] })
140
- ] }) });
293
+ ] });
294
+ });
295
+ function byNewest(a, b) {
296
+ return new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime();
141
297
  }
142
298
  var react_default = ApiKeyWidget;
143
299
  export {
@@ -17,12 +17,24 @@
17
17
  * getUser: async () => {
18
18
  * const s = await auth();
19
19
  * if (!s?.user) return null;
20
- * return { tenant: s.user.orgId ?? s.user.id, userId: s.user.id, email: s.user.email ?? undefined, label: s.user.name ?? undefined };
20
+ * return {
21
+ * tenant: s.user.orgId ?? s.user.id,
22
+ * userId: s.user.id,
23
+ * email: s.user.email ?? undefined,
24
+ * // Which key types this person may create. Omit → ['call-only'].
25
+ * // false → no API access. A list of 2+ → the widget shows a picker.
26
+ * keyTypes: s.user.isEngineer ? ['manager', 'call-only'] : undefined,
27
+ * };
21
28
  * },
22
- * resolveScopes: (u) => u.isEngineer ? ['call', 'use-premium'] : ['call'], // optional
23
29
  * });
24
30
  * export const GET = keys.handler;
25
31
  * export const POST = keys.handler;
32
+ *
33
+ * SECURITY: eligibility is decided HERE, on your server, from your session —
34
+ * never from the browser. When a user is eligible for more than one type the
35
+ * browser sends its pick, but this handler only honors it if it's in the user's
36
+ * allowed set; otherwise it's rejected. `keyTypes: false` is enforced server-side
37
+ * (403) before anything is provisioned, not merely hidden in the UI.
26
38
  */
27
39
  interface AppUser {
28
40
  /** The producer's stable id for the consumer COMPANY — becomes the tenant (isolation). */
@@ -33,18 +45,31 @@ interface AppUser {
33
45
  email?: string;
34
46
  /** Optional display name (cosmetic). */
35
47
  label?: string;
36
- /** Any extra fields your resolveScopes wants (e.g. isEngineer). */
48
+ /**
49
+ * Which key types this user may create.
50
+ * - omit / undefined → ['call-only'] (the safe default; the widget just works)
51
+ * - a list → those types (widget shows a picker when there's >1)
52
+ * - false or [] → no API access (widget shows a denied state; mint refused)
53
+ */
54
+ keyTypes?: string[] | false;
55
+ /** Any extra fields your resolveKeyTypes wants (e.g. isEngineer). */
37
56
  [k: string]: unknown;
38
57
  }
39
58
  interface ApiblazeKeysConfig {
40
- /** The producer CP key from the apiblaze dashboard Developers section. Server-only. */
59
+ /** The producer CP key from the apiblaze dashboard Developers section. Server-only.
60
+ * Use a purpose-built "widget" key (not a full admin key) so the platform's own
61
+ * subset rule is a real backstop under your eligibility logic. */
41
62
  cpKey: string;
42
63
  /** Return the logged-in user from the request/session, or null if unauthenticated. */
43
64
  getUser: (req: Request) => Promise<AppUser | null> | AppUser | null;
44
- /** Permission set to mint for a user. Default ['call']. Server-side only, so a browser
45
- * can never escalate return more for your own engineers here. */
46
- resolveScopes?: (user: AppUser) => string[] | Promise<string[]>;
47
- /** Default expiry for minted keys (seconds); enables re-reveal. Default 1 year. */
65
+ /** Optional override for eligibility, if you'd rather compute it here than inline
66
+ * in getUser. Same contract as AppUser.keyTypes (undefined default, false/[] → deny).
67
+ * Takes precedence over user.keyTypes when provided. */
68
+ resolveKeyTypes?: (user: AppUser) => (string[] | false) | Promise<string[] | false>;
69
+ /** Key lifetime in seconds. Omit for a DURABLE key (no expiry) — the right
70
+ * default for a production API key a developer embeds, so it never silently
71
+ * stops working. A durable key is shown ONCE at creation (not re-revealable);
72
+ * set an expiry if you want keys that stay revealable until they expire. */
48
73
  keyExpiresInSeconds?: number;
49
74
  /** Override the apiblaze base (tests/self-host). Default https://apikeys.apiblaze.com */
50
75
  base?: string;
@@ -53,6 +78,7 @@ type Action = {
53
78
  action: 'list';
54
79
  } | {
55
80
  action: 'create';
81
+ keyType?: string;
56
82
  } | {
57
83
  action: 'reveal';
58
84
  keyId: string;
@@ -17,12 +17,24 @@
17
17
  * getUser: async () => {
18
18
  * const s = await auth();
19
19
  * if (!s?.user) return null;
20
- * return { tenant: s.user.orgId ?? s.user.id, userId: s.user.id, email: s.user.email ?? undefined, label: s.user.name ?? undefined };
20
+ * return {
21
+ * tenant: s.user.orgId ?? s.user.id,
22
+ * userId: s.user.id,
23
+ * email: s.user.email ?? undefined,
24
+ * // Which key types this person may create. Omit → ['call-only'].
25
+ * // false → no API access. A list of 2+ → the widget shows a picker.
26
+ * keyTypes: s.user.isEngineer ? ['manager', 'call-only'] : undefined,
27
+ * };
21
28
  * },
22
- * resolveScopes: (u) => u.isEngineer ? ['call', 'use-premium'] : ['call'], // optional
23
29
  * });
24
30
  * export const GET = keys.handler;
25
31
  * export const POST = keys.handler;
32
+ *
33
+ * SECURITY: eligibility is decided HERE, on your server, from your session —
34
+ * never from the browser. When a user is eligible for more than one type the
35
+ * browser sends its pick, but this handler only honors it if it's in the user's
36
+ * allowed set; otherwise it's rejected. `keyTypes: false` is enforced server-side
37
+ * (403) before anything is provisioned, not merely hidden in the UI.
26
38
  */
27
39
  interface AppUser {
28
40
  /** The producer's stable id for the consumer COMPANY — becomes the tenant (isolation). */
@@ -33,18 +45,31 @@ interface AppUser {
33
45
  email?: string;
34
46
  /** Optional display name (cosmetic). */
35
47
  label?: string;
36
- /** Any extra fields your resolveScopes wants (e.g. isEngineer). */
48
+ /**
49
+ * Which key types this user may create.
50
+ * - omit / undefined → ['call-only'] (the safe default; the widget just works)
51
+ * - a list → those types (widget shows a picker when there's >1)
52
+ * - false or [] → no API access (widget shows a denied state; mint refused)
53
+ */
54
+ keyTypes?: string[] | false;
55
+ /** Any extra fields your resolveKeyTypes wants (e.g. isEngineer). */
37
56
  [k: string]: unknown;
38
57
  }
39
58
  interface ApiblazeKeysConfig {
40
- /** The producer CP key from the apiblaze dashboard Developers section. Server-only. */
59
+ /** The producer CP key from the apiblaze dashboard Developers section. Server-only.
60
+ * Use a purpose-built "widget" key (not a full admin key) so the platform's own
61
+ * subset rule is a real backstop under your eligibility logic. */
41
62
  cpKey: string;
42
63
  /** Return the logged-in user from the request/session, or null if unauthenticated. */
43
64
  getUser: (req: Request) => Promise<AppUser | null> | AppUser | null;
44
- /** Permission set to mint for a user. Default ['call']. Server-side only, so a browser
45
- * can never escalate return more for your own engineers here. */
46
- resolveScopes?: (user: AppUser) => string[] | Promise<string[]>;
47
- /** Default expiry for minted keys (seconds); enables re-reveal. Default 1 year. */
65
+ /** Optional override for eligibility, if you'd rather compute it here than inline
66
+ * in getUser. Same contract as AppUser.keyTypes (undefined default, false/[] → deny).
67
+ * Takes precedence over user.keyTypes when provided. */
68
+ resolveKeyTypes?: (user: AppUser) => (string[] | false) | Promise<string[] | false>;
69
+ /** Key lifetime in seconds. Omit for a DURABLE key (no expiry) — the right
70
+ * default for a production API key a developer embeds, so it never silently
71
+ * stops working. A durable key is shown ONCE at creation (not re-revealable);
72
+ * set an expiry if you want keys that stay revealable until they expire. */
48
73
  keyExpiresInSeconds?: number;
49
74
  /** Override the apiblaze base (tests/self-host). Default https://apikeys.apiblaze.com */
50
75
  base?: string;
@@ -53,6 +78,7 @@ type Action = {
53
78
  action: 'list';
54
79
  } | {
55
80
  action: 'create';
81
+ keyType?: string;
56
82
  } | {
57
83
  action: 'reveal';
58
84
  keyId: string;
@@ -23,10 +23,11 @@ __export(server_exports, {
23
23
  createApiblazeKeys: () => createApiblazeKeys
24
24
  });
25
25
  module.exports = __toCommonJS(server_exports);
26
- var YEAR = 365 * 24 * 60 * 60;
27
26
  function createApiblazeKeys(cfg) {
27
+ if (!cfg.cpKey) throw new Error("apiblaze: cpKey is required (set APIBLAZE_CP_KEY). It is server-only \u2014 never expose it to the browser.");
28
+ if (typeof cfg.getUser !== "function") throw new Error("apiblaze: getUser is required \u2014 map your session to { tenant, userId }.");
28
29
  const base = (cfg.base ?? "https://apikeys.apiblaze.com").replace(/\/$/, "");
29
- const expiresIn = cfg.keyExpiresInSeconds ?? YEAR;
30
+ const expiresIn = cfg.keyExpiresInSeconds;
30
31
  const tenantCache = /* @__PURE__ */ new Map();
31
32
  function planeFetch(method, path, user, opts = {}) {
32
33
  const params = new URLSearchParams({ product: "apiblaze", ...opts.query ?? {} });
@@ -48,20 +49,50 @@ function createApiblazeKeys(cfg) {
48
49
  tenantCache.set(user.tenant, tenant);
49
50
  return tenant;
50
51
  }
52
+ async function allowedTypesFor(user) {
53
+ const raw = cfg.resolveKeyTypes ? await cfg.resolveKeyTypes(user) : user.keyTypes;
54
+ if (raw === false) return false;
55
+ if (Array.isArray(raw)) return raw.length === 0 ? false : raw;
56
+ return ["call-only"];
57
+ }
58
+ async function catalogNames(user, tenant) {
59
+ const r = await planeFetch("GET", "/key-types", user, { query: { tenant } });
60
+ if (!r.ok) return [];
61
+ const d = await r.json();
62
+ return (d.key_types ?? []).map((t) => t.name).filter((n) => typeof n === "string");
63
+ }
51
64
  async function run(user, op) {
65
+ if (op.action === "list") {
66
+ const allowed = await allowedTypesFor(user);
67
+ if (allowed === false) return { status: 200, data: { keys: [], keyTypes: [], access: "denied" } };
68
+ const tenant2 = await resolveTenant(user);
69
+ const [listRes, catalog] = await Promise.all([
70
+ planeFetch("GET", "/apikeys", user, { query: { tenant: tenant2, limit: "200" } }),
71
+ catalogNames(user, tenant2)
72
+ ]);
73
+ const d = await listRes.json();
74
+ const eligible = allowed.filter((t) => catalog.includes(t));
75
+ return { status: listRes.status, data: { keys: d.keys ?? [], keyTypes: eligible, access: "ok" } };
76
+ }
77
+ if (op.action === "create") {
78
+ const allowed = await allowedTypesFor(user);
79
+ if (allowed === false) return { status: 403, data: { error: "You do not have API access.", access: "denied" } };
80
+ const tenant2 = await resolveTenant(user);
81
+ const catalog = await catalogNames(user, tenant2);
82
+ const eligible = allowed.filter((t) => catalog.includes(t));
83
+ if (eligible.length === 0) return { status: 409, data: { error: "No key types are available for your account." } };
84
+ if (op.keyType && !eligible.includes(op.keyType)) {
85
+ return { status: 403, data: { error: "You are not allowed to create that key type." } };
86
+ }
87
+ const chosen = op.keyType ?? (eligible.includes("call-only") ? "call-only" : eligible[0]);
88
+ const body = { tenant: tenant2, key_type_ids: [chosen], environment: "prod" };
89
+ if (typeof expiresIn === "number" && expiresIn > 0) body.expires_in_seconds = expiresIn;
90
+ const r = await planeFetch("POST", "/apikeys", user, { body });
91
+ const d = await r.json();
92
+ return { status: r.status, data: { key: d.key, key_id: d.metadata?.key_id } };
93
+ }
52
94
  const tenant = await resolveTenant(user);
53
95
  switch (op.action) {
54
- case "list": {
55
- const r = await planeFetch("GET", "/apikeys", user, { query: { tenant, limit: "200" } });
56
- const d = await r.json();
57
- return { status: r.status, data: { keys: d.keys ?? [] } };
58
- }
59
- case "create": {
60
- const permissions = await cfg.resolveScopes?.(user) ?? ["call"];
61
- const r = await planeFetch("POST", "/apikeys", user, { body: { tenant, permissions, environment: "prod", expires_in_seconds: expiresIn } });
62
- const d = await r.json();
63
- return { status: r.status, data: { key: d.key, key_id: d.metadata?.key_id } };
64
- }
65
96
  case "reveal": {
66
97
  const r = await planeFetch("GET", `/apikeys/${encodeURIComponent(op.keyId)}/reveal`, user, { query: { tenant } });
67
98
  return { status: r.status, data: await r.json().catch(() => ({})) };
@@ -91,7 +122,7 @@ function createApiblazeKeys(cfg) {
91
122
  const raw = await req.json().catch(() => ({}));
92
123
  const a = raw.action ?? "create";
93
124
  if ((a === "reveal" || a === "rotate" || a === "revoke") && !raw.keyId) return json({ error: "keyId required" }, 400);
94
- op = { action: a, keyId: raw.keyId };
125
+ op = { action: a, keyId: raw.keyId, keyType: raw.keyType };
95
126
  }
96
127
  try {
97
128
  const { status, data } = await run(user, op);
@@ -1,8 +1,9 @@
1
1
  // src/server/index.ts
2
- var YEAR = 365 * 24 * 60 * 60;
3
2
  function createApiblazeKeys(cfg) {
3
+ if (!cfg.cpKey) throw new Error("apiblaze: cpKey is required (set APIBLAZE_CP_KEY). It is server-only \u2014 never expose it to the browser.");
4
+ if (typeof cfg.getUser !== "function") throw new Error("apiblaze: getUser is required \u2014 map your session to { tenant, userId }.");
4
5
  const base = (cfg.base ?? "https://apikeys.apiblaze.com").replace(/\/$/, "");
5
- const expiresIn = cfg.keyExpiresInSeconds ?? YEAR;
6
+ const expiresIn = cfg.keyExpiresInSeconds;
6
7
  const tenantCache = /* @__PURE__ */ new Map();
7
8
  function planeFetch(method, path, user, opts = {}) {
8
9
  const params = new URLSearchParams({ product: "apiblaze", ...opts.query ?? {} });
@@ -24,20 +25,50 @@ function createApiblazeKeys(cfg) {
24
25
  tenantCache.set(user.tenant, tenant);
25
26
  return tenant;
26
27
  }
28
+ async function allowedTypesFor(user) {
29
+ const raw = cfg.resolveKeyTypes ? await cfg.resolveKeyTypes(user) : user.keyTypes;
30
+ if (raw === false) return false;
31
+ if (Array.isArray(raw)) return raw.length === 0 ? false : raw;
32
+ return ["call-only"];
33
+ }
34
+ async function catalogNames(user, tenant) {
35
+ const r = await planeFetch("GET", "/key-types", user, { query: { tenant } });
36
+ if (!r.ok) return [];
37
+ const d = await r.json();
38
+ return (d.key_types ?? []).map((t) => t.name).filter((n) => typeof n === "string");
39
+ }
27
40
  async function run(user, op) {
41
+ if (op.action === "list") {
42
+ const allowed = await allowedTypesFor(user);
43
+ if (allowed === false) return { status: 200, data: { keys: [], keyTypes: [], access: "denied" } };
44
+ const tenant2 = await resolveTenant(user);
45
+ const [listRes, catalog] = await Promise.all([
46
+ planeFetch("GET", "/apikeys", user, { query: { tenant: tenant2, limit: "200" } }),
47
+ catalogNames(user, tenant2)
48
+ ]);
49
+ const d = await listRes.json();
50
+ const eligible = allowed.filter((t) => catalog.includes(t));
51
+ return { status: listRes.status, data: { keys: d.keys ?? [], keyTypes: eligible, access: "ok" } };
52
+ }
53
+ if (op.action === "create") {
54
+ const allowed = await allowedTypesFor(user);
55
+ if (allowed === false) return { status: 403, data: { error: "You do not have API access.", access: "denied" } };
56
+ const tenant2 = await resolveTenant(user);
57
+ const catalog = await catalogNames(user, tenant2);
58
+ const eligible = allowed.filter((t) => catalog.includes(t));
59
+ if (eligible.length === 0) return { status: 409, data: { error: "No key types are available for your account." } };
60
+ if (op.keyType && !eligible.includes(op.keyType)) {
61
+ return { status: 403, data: { error: "You are not allowed to create that key type." } };
62
+ }
63
+ const chosen = op.keyType ?? (eligible.includes("call-only") ? "call-only" : eligible[0]);
64
+ const body = { tenant: tenant2, key_type_ids: [chosen], environment: "prod" };
65
+ if (typeof expiresIn === "number" && expiresIn > 0) body.expires_in_seconds = expiresIn;
66
+ const r = await planeFetch("POST", "/apikeys", user, { body });
67
+ const d = await r.json();
68
+ return { status: r.status, data: { key: d.key, key_id: d.metadata?.key_id } };
69
+ }
28
70
  const tenant = await resolveTenant(user);
29
71
  switch (op.action) {
30
- case "list": {
31
- const r = await planeFetch("GET", "/apikeys", user, { query: { tenant, limit: "200" } });
32
- const d = await r.json();
33
- return { status: r.status, data: { keys: d.keys ?? [] } };
34
- }
35
- case "create": {
36
- const permissions = await cfg.resolveScopes?.(user) ?? ["call"];
37
- const r = await planeFetch("POST", "/apikeys", user, { body: { tenant, permissions, environment: "prod", expires_in_seconds: expiresIn } });
38
- const d = await r.json();
39
- return { status: r.status, data: { key: d.key, key_id: d.metadata?.key_id } };
40
- }
41
72
  case "reveal": {
42
73
  const r = await planeFetch("GET", `/apikeys/${encodeURIComponent(op.keyId)}/reveal`, user, { query: { tenant } });
43
74
  return { status: r.status, data: await r.json().catch(() => ({})) };
@@ -67,7 +98,7 @@ function createApiblazeKeys(cfg) {
67
98
  const raw = await req.json().catch(() => ({}));
68
99
  const a = raw.action ?? "create";
69
100
  if ((a === "reveal" || a === "rotate" || a === "revoke") && !raw.keyId) return json({ error: "keyId required" }, 400);
70
- op = { action: a, keyId: raw.keyId };
101
+ op = { action: a, keyId: raw.keyId, keyType: raw.keyType };
71
102
  }
72
103
  try {
73
104
  const { status, data } = await run(user, op);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apiblaze",
3
- "version": "0.17.21",
3
+ "version": "0.17.22",
4
4
  "description": "APIblaze CLI \u2014 Chat with your APIs, Manage your API keys, users and groups with the APIblaze serverless proxy",
5
5
  "keywords": [
6
6
  "apiblaze",
@@ -21,24 +21,19 @@
21
21
  "exports": {
22
22
  ".": "./dist/index.js",
23
23
  "./sidecar": {
24
- "types": "./dist/sidecar/index.d.ts",
25
- "import": "./dist/sidecar/index.mjs",
26
- "require": "./dist/sidecar/index.js",
27
- "default": "./dist/sidecar/index.js"
24
+ "import": { "types": "./dist/sidecar/index.d.mts", "default": "./dist/sidecar/index.mjs" },
25
+ "require": { "types": "./dist/sidecar/index.d.ts", "default": "./dist/sidecar/index.js" }
28
26
  },
29
27
  "./server": {
30
- "types": "./dist/server/index.d.ts",
31
- "import": "./dist/server/index.mjs",
32
- "require": "./dist/server/index.js",
33
- "default": "./dist/server/index.js"
28
+ "import": { "types": "./dist/server/index.d.mts", "default": "./dist/server/index.mjs" },
29
+ "require": { "types": "./dist/server/index.d.ts", "default": "./dist/server/index.js" }
34
30
  },
35
31
  "./react": {
36
- "types": "./dist/react/index.d.ts",
37
- "import": "./dist/react/index.mjs",
38
- "require": "./dist/react/index.js",
39
- "default": "./dist/react/index.js"
32
+ "import": { "types": "./dist/react/index.d.mts", "default": "./dist/react/index.mjs" },
33
+ "require": { "types": "./dist/react/index.d.ts", "default": "./dist/react/index.js" }
40
34
  }
41
35
  },
36
+ "sideEffects": false,
42
37
  "files": [
43
38
  "dist/",
44
39
  "README.md"
@@ -67,8 +62,7 @@
67
62
  "@types/ws": "^8.5.10",
68
63
  "tsup": "^8.0.0",
69
64
  "typescript": "^5.4.0",
70
- "@types/react": "^18.2.0",
71
- "react": "^18.2.0"
65
+ "@types/react": "^18.2.0"
72
66
  },
73
67
  "peerDependencies": {
74
68
  "react": ">=17"