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 +1 -1
- package/dist/react/index.d.mts +34 -7
- package/dist/react/index.d.ts +34 -7
- package/dist/react/index.js +221 -66
- package/dist/react/index.mjs +222 -66
- package/dist/server/index.d.mts +34 -8
- package/dist/server/index.d.ts +34 -8
- package/dist/server/index.js +45 -14
- package/dist/server/index.mjs +45 -14
- package/package.json +9 -15
package/dist/index.js
CHANGED
package/dist/react/index.d.mts
CHANGED
|
@@ -1,32 +1,59 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* apiblaze/react — <ApiKeyWidget/>. Drop it into resiresi.com;
|
|
5
|
-
* own backend route (which holds the CP key). Shows the
|
|
6
|
-
*
|
|
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
|
-
/**
|
|
27
|
-
|
|
50
|
+
/** Heading text. Default "Your API keys". */
|
|
51
|
+
title?: string;
|
|
28
52
|
className?: string;
|
|
29
53
|
}
|
|
30
|
-
|
|
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 };
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,32 +1,59 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* apiblaze/react — <ApiKeyWidget/>. Drop it into resiresi.com;
|
|
5
|
-
* own backend route (which holds the CP key). Shows the
|
|
6
|
-
*
|
|
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
|
-
/**
|
|
27
|
-
|
|
50
|
+
/** Heading text. Default "Your API keys". */
|
|
51
|
+
title?: string;
|
|
28
52
|
className?: string;
|
|
29
53
|
}
|
|
30
|
-
|
|
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 };
|
package/dist/react/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 [
|
|
53
|
-
const [
|
|
54
|
-
const [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
+
setRowError({ keyId: "", msg: e instanceof Error ? e.message : "Could not create a key." });
|
|
83
119
|
} finally {
|
|
84
|
-
|
|
120
|
+
setCreating(false);
|
|
85
121
|
}
|
|
86
122
|
}
|
|
87
123
|
async function reveal(keyId) {
|
|
88
|
-
|
|
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
|
|
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
|
-
|
|
131
|
+
setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not show this key." });
|
|
95
132
|
} finally {
|
|
96
|
-
|
|
133
|
+
setPending(null);
|
|
97
134
|
}
|
|
98
135
|
}
|
|
99
136
|
async function rotate(keyId) {
|
|
100
|
-
|
|
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
|
-
|
|
145
|
+
setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not rotate this key." });
|
|
107
146
|
} finally {
|
|
108
|
-
|
|
147
|
+
setPending(null);
|
|
109
148
|
}
|
|
110
149
|
}
|
|
111
150
|
async function revoke(keyId) {
|
|
112
|
-
|
|
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
|
-
|
|
158
|
+
setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not revoke this key." });
|
|
118
159
|
} finally {
|
|
119
|
-
|
|
160
|
+
setPending(null);
|
|
120
161
|
}
|
|
121
162
|
}
|
|
122
|
-
function copy(v) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
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:
|
|
182
|
+
fontFamily: t.fontFamily,
|
|
138
183
|
fontSize: 14
|
|
139
184
|
};
|
|
140
|
-
const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius,
|
|
141
|
-
const btn = (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
] },
|
|
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:
|
package/dist/react/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 [
|
|
18
|
-
const [
|
|
19
|
-
const [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
setRowError({ keyId: "", msg: e instanceof Error ? e.message : "Could not create a key." });
|
|
48
85
|
} finally {
|
|
49
|
-
|
|
86
|
+
setCreating(false);
|
|
50
87
|
}
|
|
51
88
|
}
|
|
52
89
|
async function reveal(keyId) {
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
97
|
+
setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not show this key." });
|
|
60
98
|
} finally {
|
|
61
|
-
|
|
99
|
+
setPending(null);
|
|
62
100
|
}
|
|
63
101
|
}
|
|
64
102
|
async function rotate(keyId) {
|
|
65
|
-
|
|
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
|
-
|
|
111
|
+
setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not rotate this key." });
|
|
72
112
|
} finally {
|
|
73
|
-
|
|
113
|
+
setPending(null);
|
|
74
114
|
}
|
|
75
115
|
}
|
|
76
116
|
async function revoke(keyId) {
|
|
77
|
-
|
|
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
|
-
|
|
124
|
+
setRowError({ keyId, msg: e instanceof Error ? e.message : "Could not revoke this key." });
|
|
83
125
|
} finally {
|
|
84
|
-
|
|
126
|
+
setPending(null);
|
|
85
127
|
}
|
|
86
128
|
}
|
|
87
|
-
function copy(v) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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:
|
|
148
|
+
fontFamily: t.fontFamily,
|
|
103
149
|
fontSize: 14
|
|
104
150
|
};
|
|
105
|
-
const card = { background: t.surface, border: `1px solid ${t.border}`, borderRadius: t.radius,
|
|
106
|
-
const btn = (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 {
|
package/dist/server/index.d.mts
CHANGED
|
@@ -17,12 +17,24 @@
|
|
|
17
17
|
* getUser: async () => {
|
|
18
18
|
* const s = await auth();
|
|
19
19
|
* if (!s?.user) return null;
|
|
20
|
-
* return {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
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;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -17,12 +17,24 @@
|
|
|
17
17
|
* getUser: async () => {
|
|
18
18
|
* const s = await auth();
|
|
19
19
|
* if (!s?.user) return null;
|
|
20
|
-
* return {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
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;
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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);
|
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
25
|
-
"
|
|
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.
|
|
31
|
-
"
|
|
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.
|
|
37
|
-
"
|
|
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"
|