@burdenoff/microfe-devportal 2026.525.3 → 2026.528.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,28 +1,14 @@
1
- import { cn as e } from "../utils/cn.js";
2
- import { jsx as t, jsxs as n } from "react/jsx-runtime";
1
+ import { jsx as e } from "react/jsx-runtime";
2
+ import { EmptyState as t } from "@burdenoff/fe-libs/ui";
3
3
  //#region src/devportal/components/EmptyState.tsx
4
- var r = ({ icon: r, title: i, description: a, action: o, className: s }) => /* @__PURE__ */ n("div", {
5
- className: e("flex flex-col items-center justify-center rounded-lg border border-dashed bg-muted/10 p-12 text-center", s),
6
- children: [
7
- r && /* @__PURE__ */ t("div", {
8
- className: "mb-4 rounded-full bg-muted p-4 text-muted-foreground",
9
- children: r
10
- }),
11
- /* @__PURE__ */ t("h3", {
12
- className: "text-lg font-semibold",
13
- children: i
14
- }),
15
- a && /* @__PURE__ */ t("p", {
16
- className: "mt-2 max-w-md text-sm text-muted-foreground",
17
- children: a
18
- }),
19
- o && /* @__PURE__ */ t("div", {
20
- className: "mt-6",
21
- children: o
22
- })
23
- ]
4
+ var n = ({ icon: n, title: r, description: i, action: a, className: o }) => /* @__PURE__ */ e(t, {
5
+ icon: n,
6
+ title: r,
7
+ description: i,
8
+ action: a,
9
+ className: o
24
10
  });
25
11
  //#endregion
26
- export { r as EmptyState };
12
+ export { n as EmptyState };
27
13
 
28
14
  //# sourceMappingURL=EmptyState.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/devportal/components/EmptyState.tsx"],"sourcesContent":["import type { FC, ReactNode } from \"react\";\nimport { cn } from \"../utils/cn\";\n\ninterface EmptyStateProps {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n}\n\n/**\n * Empty state component for when there's no data to display\n */\nexport const EmptyState: FC<EmptyStateProps> = ({\n icon,\n title,\n description,\n action,\n className,\n}) => {\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center rounded-lg border border-dashed bg-muted/10 p-12 text-center\",\n className,\n )}\n >\n {icon && (\n <div className=\"mb-4 rounded-full bg-muted p-4 text-muted-foreground\">\n {icon}\n </div>\n )}\n <h3 className=\"text-lg font-semibold\">{title}</h3>\n {description && (\n <p className=\"mt-2 max-w-md text-sm text-muted-foreground\">\n {description}\n </p>\n )}\n {action && <div className=\"mt-6\">{action}</div>}\n </div>\n );\n};\n"],"mappings":";;;AAcA,IAAa,KAAmC,EAC9C,SACA,UACA,gBACA,WACA,mBAGE,kBAAC,OAAD;CACE,WAAW,EACT,0GACA,EACD;WAJH;EAMG,KACC,kBAAC,OAAD;GAAK,WAAU;aACZ;GACG,CAAA;EAER,kBAAC,MAAD;GAAI,WAAU;aAAyB;GAAW,CAAA;EACjD,KACC,kBAAC,KAAD;GAAG,WAAU;aACV;GACC,CAAA;EAEL,KAAU,kBAAC,OAAD;GAAK,WAAU;aAAQ;GAAa,CAAA;EAC3C"}
1
+ {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/devportal/components/EmptyState.tsx"],"sourcesContent":["import type { FC, ReactNode } from \"react\";\nimport { EmptyState as CanonicalEmptyState } from \"@burdenoff/fe-libs/ui\";\n\ninterface EmptyStateProps {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n}\n\n/**\n * Local wrapper around `EmptyState` from `@burdenoff/fe-libs/ui`.\n *\n * Keeps existing devportal call sites stable while routing all empty-state\n * rendering through the canonical primitive.\n */\nexport const EmptyState: FC<EmptyStateProps> = ({\n icon,\n title,\n description,\n action,\n className,\n}) => {\n return (\n <CanonicalEmptyState\n icon={icon}\n title={title}\n description={description}\n action={action}\n className={className}\n />\n );\n};\n"],"mappings":";;;AAiBA,IAAa,KAAmC,EAC9C,SACA,UACA,gBACA,WACA,mBAGE,kBAAC,GAAD;CACQ;CACC;CACM;CACL;CACG;CACX,CAAA"}
@@ -14,8 +14,9 @@ import { useState as m } from "react";
14
14
  import { useI18n as h } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
15
15
  import { Loader2 as g, Save as _, X as v } from "lucide-react";
16
16
  import { jsx as y, jsxs as b } from "react/jsx-runtime";
17
+ import { OptionalFields as x } from "@burdenoff/fe-libs/ui";
17
18
  //#region src/devportal/pages/CreateApiKeyPage.tsx
18
- var x = {
19
+ var S = {
19
20
  "30days": 720 * 60 * 60 * 1e3,
20
21
  "90days": 2160 * 60 * 60 * 1e3,
21
22
  "180days": 4320 * 60 * 60 * 1e3,
@@ -23,64 +24,64 @@ var x = {
23
24
  "2years": 2 * 365 * 24 * 60 * 60 * 1e3,
24
25
  never: null
25
26
  };
26
- function S(e) {
27
- let t = x[e];
27
+ function C(e) {
28
+ let t = S[e];
28
29
  if (t != null) return new Date(Date.now() + t).toISOString();
29
30
  }
30
- var C = () => {
31
- let { t: x } = h(), C = (e, t) => {
32
- let n = x(e);
31
+ var w = () => {
32
+ let { t: S } = h(), w = (e, t) => {
33
+ let n = S(e);
33
34
  return n === e ? t : n;
34
- }, { navigate: w, basePath: T } = e(), [E, D] = m(null), [O, k] = m({}), [A, j] = m(null), [M, { loading: N }] = u(), P = t(), { data: F, loading: I } = l({ fetchPolicy: "cache-and-network" }), L = F?.currentUserApps?.map((e) => ({
35
+ }, { navigate: T, basePath: E } = e(), [D, O] = m(null), [k, A] = m({}), [j, M] = m(null), [N, { loading: P }] = u(), F = t(), { data: I, loading: L } = l({ fetchPolicy: "cache-and-network" }), R = I?.currentUserApps?.map((e) => ({
35
36
  id: e.id,
36
37
  name: e.name,
37
38
  clientId: e.clientId
38
- })) ?? [], [R, z] = m({
39
+ })) ?? [], [z, B] = m({
39
40
  appId: "",
40
41
  name: "",
41
42
  scopes: [],
42
43
  expiresIn: "1year"
43
- }), B = (e) => {
44
- w?.(`${T}${e}`);
45
- }, V = () => {
46
- B("/api-keys");
47
- }, H = (e, t) => {
48
- z((n) => ({
44
+ }), V = (e) => {
45
+ T?.(`${E}${e}`);
46
+ }, H = () => {
47
+ V("/api-keys");
48
+ }, U = (e, t) => {
49
+ B((n) => ({
49
50
  ...n,
50
51
  [e]: t
51
- })), O[e] && k((t) => ({
52
+ })), k[e] && A((t) => ({
52
53
  ...t,
53
54
  [e]: ""
54
- })), A && j(null);
55
- }, U = () => {
55
+ })), j && M(null);
56
+ }, W = () => {
56
57
  let e = {};
57
- return R.appId || (e.appId = C("devportal.selectAppError", "Please select an app to associate this key with")), R.name.trim() || (e.name = C("devportal.apiKeyNameRequired", "API key name is required")), R.scopes.length === 0 && (e.scopes = C("devportal.atLeastOneScopeRequired", "At least one scope is required")), k(e), Object.keys(e).length === 0;
58
- }, W = async (e) => {
59
- if (e.preventDefault(), U()) {
60
- j(null);
58
+ return z.appId || (e.appId = w("devportal.selectAppError", "Please select an app to associate this key with")), z.name.trim() || (e.name = w("devportal.apiKeyNameRequired", "API key name is required")), z.scopes.length === 0 && (e.scopes = w("devportal.atLeastOneScopeRequired", "At least one scope is required")), A(e), Object.keys(e).length === 0;
59
+ }, G = async (e) => {
60
+ if (e.preventDefault(), W()) {
61
+ M(null);
61
62
  try {
62
- let e = S(R.expiresIn), t = await p("api_key_create"), { data: n } = await M({ variables: { input: {
63
- appId: R.appId,
64
- name: R.name.trim(),
65
- scope: R.scopes,
63
+ let e = C(z.expiresIn), t = await p("api_key_create"), { data: n } = await N({ variables: { input: {
64
+ appId: z.appId,
65
+ name: z.name.trim(),
66
+ scope: z.scopes,
66
67
  expiresAt: e,
67
68
  ...t ? { recaptchaToken: t } : {}
68
69
  } } });
69
- n?.generateAPIKey && (P.emit("devportal.apikey.generated", {
70
+ n?.generateAPIKey && (F.emit("devportal.apikey.generated", {
70
71
  keyId: n.generateAPIKey.keyId,
71
- appId: R.appId
72
- }), D({
72
+ appId: z.appId
73
+ }), O({
73
74
  apiKey: n.generateAPIKey.apiKey,
74
75
  apiSecret: n.generateAPIKey.apiSecret,
75
76
  keyId: n.generateAPIKey.keyId
76
77
  }));
77
78
  } catch (e) {
78
- j(e instanceof Error ? e.message : C("devportal.failedToGenerateApiKey", "Failed to generate API key"));
79
+ M(e instanceof Error ? e.message : w("devportal.failedToGenerateApiKey", "Failed to generate API key"));
79
80
  }
80
81
  }
81
- }, G = () => {
82
- B("/api-keys");
83
- }, K = [
82
+ }, K = () => {
83
+ V("/api-keys");
84
+ }, q = [
84
85
  {
85
86
  value: "30days",
86
87
  label: "30 days"
@@ -106,7 +107,7 @@ var C = () => {
106
107
  label: "Never expires"
107
108
  }
108
109
  ];
109
- return E ? /* @__PURE__ */ y(n, {
110
+ return D ? /* @__PURE__ */ y(n, {
110
111
  title: "API Key Generated",
111
112
  description: "Your new API key has been generated successfully",
112
113
  children: /* @__PURE__ */ b("div", {
@@ -114,9 +115,9 @@ var C = () => {
114
115
  children: [
115
116
  /* @__PURE__ */ y(i, {
116
117
  title: "API Key & Secret",
117
- secret: E.apiSecret,
118
+ secret: D.apiSecret,
118
119
  description: "Store this secret securely — you won't be able to see it again.",
119
- onDismiss: G
120
+ onDismiss: K
120
121
  }),
121
122
  /* @__PURE__ */ y(c, {
122
123
  variant: "info",
@@ -134,7 +135,7 @@ var C = () => {
134
135
  children: "Key ID:"
135
136
  }), /* @__PURE__ */ y("code", {
136
137
  className: "rounded bg-muted px-2 py-0.5 text-sm",
137
- children: E.keyId
138
+ children: D.keyId
138
139
  })]
139
140
  }), /* @__PURE__ */ b("div", {
140
141
  className: "flex items-center gap-2",
@@ -143,7 +144,7 @@ var C = () => {
143
144
  children: "API Key:"
144
145
  }), /* @__PURE__ */ y("code", {
145
146
  className: "rounded bg-muted px-2 py-0.5 text-sm",
146
- children: E.apiKey
147
+ children: D.apiKey
147
148
  })]
148
149
  })]
149
150
  }),
@@ -157,7 +158,7 @@ var C = () => {
157
158
  /* @__PURE__ */ y("div", {
158
159
  className: "flex gap-3",
159
160
  children: /* @__PURE__ */ y("button", {
160
- onClick: G,
161
+ onClick: K,
161
162
  className: "inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors",
162
163
  children: "Continue to API Keys"
163
164
  })
@@ -172,27 +173,27 @@ var C = () => {
172
173
  backLink: "/api-keys",
173
174
  backLabel: "Back to API Keys",
174
175
  children: /* @__PURE__ */ b("form", {
175
- onSubmit: W,
176
+ onSubmit: G,
176
177
  className: "max-w-3xl space-y-8",
177
178
  children: [
178
- A && /* @__PURE__ */ y(c, {
179
+ j && /* @__PURE__ */ y(c, {
179
180
  variant: "error",
180
181
  title: "Error Generating API Key",
181
- onDismiss: () => j(null),
182
- children: A
182
+ onDismiss: () => M(null),
183
+ children: j
183
184
  }),
184
185
  /* @__PURE__ */ y(r, {
185
186
  title: "Basic Information",
186
187
  description: "Configure the API key settings",
187
188
  children: /* @__PURE__ */ b("div", {
188
189
  className: "space-y-4",
189
- children: [I ? /* @__PURE__ */ b("div", {
190
+ children: [L ? /* @__PURE__ */ b("div", {
190
191
  className: "flex items-center gap-2 py-2",
191
192
  children: [/* @__PURE__ */ y(g, { className: "h-4 w-4 animate-spin text-muted-foreground" }), /* @__PURE__ */ y("span", {
192
193
  className: "text-sm text-muted-foreground",
193
194
  children: "Loading apps..."
194
195
  })]
195
- }) : L.length === 0 ? /* @__PURE__ */ y(c, {
196
+ }) : R.length === 0 ? /* @__PURE__ */ y(c, {
196
197
  variant: "warning",
197
198
  title: "No Apps Found",
198
199
  children: /* @__PURE__ */ b("p", { children: [
@@ -200,31 +201,31 @@ var C = () => {
200
201
  " ",
201
202
  /* @__PURE__ */ y("button", {
202
203
  type: "button",
203
- onClick: () => B("/oauth/new"),
204
+ onClick: () => V("/oauth/new"),
204
205
  className: "text-primary underline hover:no-underline",
205
206
  children: "Create an app"
206
207
  })
207
208
  ] })
208
209
  }) : /* @__PURE__ */ y(o, {
209
210
  label: "App",
210
- value: R.appId,
211
- onChange: (e) => H("appId", e.target.value),
211
+ value: z.appId,
212
+ onChange: (e) => U("appId", e.target.value),
212
213
  options: [{
213
214
  value: "",
214
215
  label: "Select an app..."
215
- }, ...L.map((e) => ({
216
+ }, ...R.map((e) => ({
216
217
  value: e.id,
217
218
  label: `${e.name} (${e.clientId})`
218
219
  }))],
219
- error: O.appId,
220
+ error: k.appId,
220
221
  required: !0,
221
222
  hint: "The OAuth app this API key will be associated with"
222
223
  }), /* @__PURE__ */ y(a, {
223
224
  label: "Key Name",
224
225
  placeholder: "e.g., Production Backend Key",
225
- value: R.name,
226
- onChange: (e) => H("name", e.target.value),
227
- error: O.name,
226
+ value: z.name,
227
+ onChange: (e) => U("name", e.target.value),
228
+ error: k.name,
228
229
  required: !0,
229
230
  hint: "A descriptive name to identify this key"
230
231
  })]
@@ -237,41 +238,45 @@ var C = () => {
237
238
  className: "space-y-4",
238
239
  children: /* @__PURE__ */ y(s, {
239
240
  label: "Scopes",
240
- value: R.scopes,
241
- onChange: (e) => H("scopes", e),
241
+ value: z.scopes,
242
+ onChange: (e) => U("scopes", e),
242
243
  placeholder: "e.g., workspace:read, vibecontrols:write",
243
244
  hint: "Press Enter to add each scope. At least one scope is required.",
244
- error: O.scopes
245
+ error: k.scopes
245
246
  })
246
247
  })
247
248
  }),
248
- /* @__PURE__ */ b(r, {
249
- title: "Key Expiration",
250
- description: "Set when this key should expire",
251
- children: [/* @__PURE__ */ y(o, {
252
- label: "Expires In",
253
- value: R.expiresIn,
254
- onChange: (e) => H("expiresIn", e.target.value),
255
- options: K,
256
- hint: "Keys can be revoked at any time regardless of expiration"
257
- }), R.expiresIn === "never" && /* @__PURE__ */ y(c, {
258
- variant: "warning",
259
- className: "mt-4",
260
- children: "Non-expiring keys are a security risk. Consider setting an expiration and rotating keys periodically."
261
- })]
249
+ /* @__PURE__ */ y(x, {
250
+ title: "Advanced settings",
251
+ description: "Rate-limit overrides, allowed origins, scope tuning, and expiration.",
252
+ children: /* @__PURE__ */ b(r, {
253
+ title: "Key Expiration",
254
+ description: "Set when this key should expire",
255
+ children: [/* @__PURE__ */ y(o, {
256
+ label: "Expires In",
257
+ value: z.expiresIn,
258
+ onChange: (e) => U("expiresIn", e.target.value),
259
+ options: q,
260
+ hint: "Keys can be revoked at any time regardless of expiration"
261
+ }), z.expiresIn === "never" && /* @__PURE__ */ y(c, {
262
+ variant: "warning",
263
+ className: "mt-4",
264
+ children: "Non-expiring keys are a security risk. Consider setting an expiration and rotating keys periodically."
265
+ })]
266
+ })
262
267
  }),
263
268
  /* @__PURE__ */ b("div", {
264
269
  className: "flex items-center justify-end gap-3 border-t pt-6",
265
270
  children: [/* @__PURE__ */ b("button", {
266
271
  type: "button",
267
- onClick: V,
272
+ onClick: H,
268
273
  className: "inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors",
269
274
  children: [/* @__PURE__ */ y(v, { size: 16 }), "Cancel"]
270
275
  }), /* @__PURE__ */ b("button", {
271
276
  type: "submit",
272
- disabled: N || L.length === 0,
277
+ disabled: P || R.length === 0,
273
278
  className: "inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50",
274
- children: [/* @__PURE__ */ y(_, { size: 16 }), N ? "Generating..." : "Generate API Key"]
279
+ children: [/* @__PURE__ */ y(_, { size: 16 }), P ? "Generating..." : "Generate API Key"]
275
280
  })]
276
281
  })
277
282
  ]
@@ -280,6 +285,6 @@ var C = () => {
280
285
  });
281
286
  };
282
287
  //#endregion
283
- export { C as CreateApiKeyPage };
288
+ export { w as CreateApiKeyPage };
284
289
 
285
290
  //# sourceMappingURL=CreateApiKeyPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CreateApiKeyPage.js","names":[],"sources":["../../../src/devportal/pages/CreateApiKeyPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { Save, X, Loader2 } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { InputField, SelectField, TagInput } from \"../components/FormField\";\nimport { NewSecretAlert } from \"../components/SecretDisplay\";\nimport { Alert } from \"../components/ConfirmDialog\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useGenerateApiKeyMutation, useCurrentUserAppsQuery } from \"../hooks\";\nimport { useDevportalEventBus } from \"../events\";\nimport { executeRecaptcha } from \"../utils/recaptcha\";\n\ninterface CreateApiKeyFormData {\n appId: string;\n name: string;\n scopes: string[];\n expiresIn: string;\n}\n\nconst EXPIRATION_DURATIONS: Record<string, number | null> = {\n \"30days\": 30 * 24 * 60 * 60 * 1000,\n \"90days\": 90 * 24 * 60 * 60 * 1000,\n \"180days\": 180 * 24 * 60 * 60 * 1000,\n \"1year\": 365 * 24 * 60 * 60 * 1000,\n \"2years\": 2 * 365 * 24 * 60 * 60 * 1000,\n never: null,\n};\n\nfunction computeExpiresAt(expiresIn: string): string | undefined {\n const duration = EXPIRATION_DURATIONS[expiresIn];\n if (duration == null) return undefined;\n return new Date(Date.now() + duration).toISOString();\n}\n\n/**\n * Page for generating a new API key via auth-svc.\n *\n * Previously created API keys through devportal-svc per application;\n * now generates keys through auth-svc's generateAPIKey mutation.\n * The user selects which OAuth app to associate the key with.\n */\nexport const CreateApiKeyPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { navigate, basePath } = useDevportal();\n const [newApiKey, setNewApiKey] = useState<{\n apiKey: string;\n apiSecret: string;\n keyId: string;\n } | null>(null);\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [mutationError, setMutationError] = useState<string | null>(null);\n\n const [generateApiKey, { loading: isSubmitting }] =\n useGenerateApiKeyMutation();\n const bus = useDevportalEventBus();\n\n // Fetch user's apps to populate the app selector\n const { data: appsData, loading: appsLoading } = useCurrentUserAppsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const apps: Array<{ id: string; name: string; clientId: string }> =\n appsData?.currentUserApps?.map(\n (app: { id: string; name: string; clientId: string }) => ({\n id: app.id,\n name: app.name,\n clientId: app.clientId,\n }),\n ) ?? [];\n\n const [formData, setFormData] = useState<CreateApiKeyFormData>({\n appId: \"\",\n name: \"\",\n scopes: [],\n expiresIn: \"1year\",\n });\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n const handleCancel = () => {\n handleNavigate(\"/api-keys\");\n };\n\n const updateField = <K extends keyof CreateApiKeyFormData>(\n field: K,\n value: CreateApiKeyFormData[K],\n ) => {\n setFormData((prev) => ({ ...prev, [field]: value }));\n if (errors[field]) {\n setErrors((prev) => ({ ...prev, [field]: \"\" }));\n }\n if (mutationError) {\n setMutationError(null);\n }\n };\n\n const validateForm = (): boolean => {\n const newErrors: Record<string, string> = {};\n\n if (!formData.appId) {\n newErrors.appId = tr(\n \"devportal.selectAppError\",\n \"Please select an app to associate this key with\",\n );\n }\n\n if (!formData.name.trim()) {\n newErrors.name = tr(\n \"devportal.apiKeyNameRequired\",\n \"API key name is required\",\n );\n }\n\n if (formData.scopes.length === 0) {\n newErrors.scopes = tr(\n \"devportal.atLeastOneScopeRequired\",\n \"At least one scope is required\",\n );\n }\n\n setErrors(newErrors);\n return Object.keys(newErrors).length === 0;\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!validateForm()) return;\n\n setMutationError(null);\n\n try {\n const expiresAt = computeExpiresAt(formData.expiresIn);\n const recaptchaToken = await executeRecaptcha(\"api_key_create\");\n const input = {\n appId: formData.appId,\n name: formData.name.trim(),\n scope: formData.scopes,\n expiresAt,\n ...(recaptchaToken ? { recaptchaToken } : {}),\n };\n\n const { data } = await generateApiKey({\n variables: {\n input,\n },\n });\n\n if (data?.generateAPIKey) {\n bus.emit(\"devportal.apikey.generated\", {\n keyId: data.generateAPIKey.keyId,\n appId: formData.appId,\n });\n setNewApiKey({\n apiKey: data.generateAPIKey.apiKey,\n apiSecret: data.generateAPIKey.apiSecret,\n keyId: data.generateAPIKey.keyId,\n });\n }\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : tr(\n \"devportal.failedToGenerateApiKey\",\n \"Failed to generate API key\",\n );\n setMutationError(message);\n }\n };\n\n const handleKeyDismiss = () => {\n handleNavigate(\"/api-keys\");\n };\n\n const expirationOptions = [\n { value: \"30days\", label: \"30 days\" },\n { value: \"90days\", label: \"90 days\" },\n { value: \"180days\", label: \"180 days\" },\n { value: \"1year\", label: \"1 year\" },\n { value: \"2years\", label: \"2 years\" },\n { value: \"never\", label: \"Never expires\" },\n ];\n\n if (newApiKey) {\n return (\n <PageLayout\n title=\"API Key Generated\"\n description=\"Your new API key has been generated successfully\"\n >\n <div className=\"max-w-2xl space-y-6\">\n <NewSecretAlert\n title=\"API Key & Secret\"\n secret={newApiKey.apiSecret}\n description=\"Store this secret securely — you won't be able to see it again.\"\n onDismiss={handleKeyDismiss}\n />\n\n <Alert variant=\"info\" title=\"Usage Instructions\">\n <div className=\"mt-2 space-y-2\">\n <p>Your API key details:</p>\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n Key ID:\n </span>\n <code className=\"rounded bg-muted px-2 py-0.5 text-sm\">\n {newApiKey.keyId}\n </code>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n API Key:\n </span>\n <code className=\"rounded bg-muted px-2 py-0.5 text-sm\">\n {newApiKey.apiKey}\n </code>\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground mt-2\">\n Use the API key and secret to authenticate your requests.\n </p>\n </div>\n </Alert>\n\n <div className=\"flex gap-3\">\n <button\n onClick={handleKeyDismiss}\n className=\"inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors\"\n >\n Continue to API Keys\n </button>\n </div>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.APP_UPDATE}>\n <PageLayout\n title=\"Generate API Key\"\n description=\"Generate a new API key for programmatic access\"\n backLink=\"/api-keys\"\n backLabel=\"Back to API Keys\"\n >\n <form onSubmit={handleSubmit} className=\"max-w-3xl space-y-8\">\n {/* Mutation error */}\n {mutationError && (\n <Alert\n variant=\"error\"\n title=\"Error Generating API Key\"\n onDismiss={() => setMutationError(null)}\n >\n {mutationError}\n </Alert>\n )}\n\n {/* Basic Information */}\n <PageSection\n title=\"Basic Information\"\n description=\"Configure the API key settings\"\n >\n <div className=\"space-y-4\">\n {/* App Selector */}\n {appsLoading ? (\n <div className=\"flex items-center gap-2 py-2\">\n <Loader2 className=\"h-4 w-4 animate-spin text-muted-foreground\" />\n <span className=\"text-sm text-muted-foreground\">\n Loading apps...\n </span>\n </div>\n ) : apps.length === 0 ? (\n <Alert variant=\"warning\" title=\"No Apps Found\">\n <p>\n You need to create an OAuth app first before generating API\n keys.{\" \"}\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"text-primary underline hover:no-underline\"\n >\n Create an app\n </button>\n </p>\n </Alert>\n ) : (\n <SelectField\n label=\"App\"\n value={formData.appId}\n onChange={(e) => updateField(\"appId\", e.target.value)}\n options={[\n { value: \"\", label: \"Select an app...\" },\n ...apps.map((app) => ({\n value: app.id,\n label: `${app.name} (${app.clientId})`,\n })),\n ]}\n error={errors.appId}\n required\n hint=\"The OAuth app this API key will be associated with\"\n />\n )}\n\n <InputField\n label=\"Key Name\"\n placeholder=\"e.g., Production Backend Key\"\n value={formData.name}\n onChange={(e) => updateField(\"name\", e.target.value)}\n error={errors.name}\n required\n hint=\"A descriptive name to identify this key\"\n />\n </div>\n </PageSection>\n\n {/* Scopes */}\n <PageSection\n title=\"Scopes\"\n description=\"Define what this API key can access\"\n >\n <div className=\"space-y-4\">\n <TagInput\n label=\"Scopes\"\n value={formData.scopes}\n onChange={(values) => updateField(\"scopes\", values)}\n placeholder=\"e.g., workspace:read, vibecontrols:write\"\n hint=\"Press Enter to add each scope. At least one scope is required.\"\n error={errors.scopes}\n />\n </div>\n </PageSection>\n\n {/* Expiration */}\n <PageSection\n title=\"Key Expiration\"\n description=\"Set when this key should expire\"\n >\n <SelectField\n label=\"Expires In\"\n value={formData.expiresIn}\n onChange={(e) => updateField(\"expiresIn\", e.target.value)}\n options={expirationOptions}\n hint=\"Keys can be revoked at any time regardless of expiration\"\n />\n\n {formData.expiresIn === \"never\" && (\n <Alert variant=\"warning\" className=\"mt-4\">\n Non-expiring keys are a security risk. Consider setting an\n expiration and rotating keys periodically.\n </Alert>\n )}\n </PageSection>\n\n {/* Actions */}\n <div className=\"flex items-center justify-end gap-3 border-t pt-6\">\n <button\n type=\"button\"\n onClick={handleCancel}\n className=\"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors\"\n >\n <X size={16} />\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting || apps.length === 0}\n className=\"inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50\"\n >\n <Save size={16} />\n {isSubmitting ? \"Generating...\" : \"Generate API Key\"}\n </button>\n </div>\n </form>\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"CreateApiKeyPage.js","names":[],"sources":["../../../src/devportal/pages/CreateApiKeyPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useState } from \"react\";\nimport { Save, X, Loader2 } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { OptionalFields } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { InputField, SelectField, TagInput } from \"../components/FormField\";\nimport { NewSecretAlert } from \"../components/SecretDisplay\";\nimport { Alert } from \"../components/ConfirmDialog\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport { useGenerateApiKeyMutation, useCurrentUserAppsQuery } from \"../hooks\";\nimport { useDevportalEventBus } from \"../events\";\nimport { executeRecaptcha } from \"../utils/recaptcha\";\n\ninterface CreateApiKeyFormData {\n appId: string;\n name: string;\n scopes: string[];\n expiresIn: string;\n}\n\nconst EXPIRATION_DURATIONS: Record<string, number | null> = {\n \"30days\": 30 * 24 * 60 * 60 * 1000,\n \"90days\": 90 * 24 * 60 * 60 * 1000,\n \"180days\": 180 * 24 * 60 * 60 * 1000,\n \"1year\": 365 * 24 * 60 * 60 * 1000,\n \"2years\": 2 * 365 * 24 * 60 * 60 * 1000,\n never: null,\n};\n\nfunction computeExpiresAt(expiresIn: string): string | undefined {\n const duration = EXPIRATION_DURATIONS[expiresIn];\n if (duration == null) return undefined;\n return new Date(Date.now() + duration).toISOString();\n}\n\n/**\n * Page for generating a new API key via auth-svc.\n *\n * Previously created API keys through devportal-svc per application;\n * now generates keys through auth-svc's generateAPIKey mutation.\n * The user selects which OAuth app to associate the key with.\n */\nexport const CreateApiKeyPage: FC = () => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { navigate, basePath } = useDevportal();\n const [newApiKey, setNewApiKey] = useState<{\n apiKey: string;\n apiSecret: string;\n keyId: string;\n } | null>(null);\n const [errors, setErrors] = useState<Record<string, string>>({});\n const [mutationError, setMutationError] = useState<string | null>(null);\n\n const [generateApiKey, { loading: isSubmitting }] =\n useGenerateApiKeyMutation();\n const bus = useDevportalEventBus();\n\n // Fetch user's apps to populate the app selector\n const { data: appsData, loading: appsLoading } = useCurrentUserAppsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const apps: Array<{ id: string; name: string; clientId: string }> =\n appsData?.currentUserApps?.map(\n (app: { id: string; name: string; clientId: string }) => ({\n id: app.id,\n name: app.name,\n clientId: app.clientId,\n }),\n ) ?? [];\n\n const [formData, setFormData] = useState<CreateApiKeyFormData>({\n appId: \"\",\n name: \"\",\n scopes: [],\n expiresIn: \"1year\",\n });\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n const handleCancel = () => {\n handleNavigate(\"/api-keys\");\n };\n\n const updateField = <K extends keyof CreateApiKeyFormData>(\n field: K,\n value: CreateApiKeyFormData[K],\n ) => {\n setFormData((prev) => ({ ...prev, [field]: value }));\n if (errors[field]) {\n setErrors((prev) => ({ ...prev, [field]: \"\" }));\n }\n if (mutationError) {\n setMutationError(null);\n }\n };\n\n const validateForm = (): boolean => {\n const newErrors: Record<string, string> = {};\n\n if (!formData.appId) {\n newErrors.appId = tr(\n \"devportal.selectAppError\",\n \"Please select an app to associate this key with\",\n );\n }\n\n if (!formData.name.trim()) {\n newErrors.name = tr(\n \"devportal.apiKeyNameRequired\",\n \"API key name is required\",\n );\n }\n\n if (formData.scopes.length === 0) {\n newErrors.scopes = tr(\n \"devportal.atLeastOneScopeRequired\",\n \"At least one scope is required\",\n );\n }\n\n setErrors(newErrors);\n return Object.keys(newErrors).length === 0;\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (!validateForm()) return;\n\n setMutationError(null);\n\n try {\n const expiresAt = computeExpiresAt(formData.expiresIn);\n const recaptchaToken = await executeRecaptcha(\"api_key_create\");\n const input = {\n appId: formData.appId,\n name: formData.name.trim(),\n scope: formData.scopes,\n expiresAt,\n ...(recaptchaToken ? { recaptchaToken } : {}),\n };\n\n const { data } = await generateApiKey({\n variables: {\n input,\n },\n });\n\n if (data?.generateAPIKey) {\n bus.emit(\"devportal.apikey.generated\", {\n keyId: data.generateAPIKey.keyId,\n appId: formData.appId,\n });\n setNewApiKey({\n apiKey: data.generateAPIKey.apiKey,\n apiSecret: data.generateAPIKey.apiSecret,\n keyId: data.generateAPIKey.keyId,\n });\n }\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : tr(\n \"devportal.failedToGenerateApiKey\",\n \"Failed to generate API key\",\n );\n setMutationError(message);\n }\n };\n\n const handleKeyDismiss = () => {\n handleNavigate(\"/api-keys\");\n };\n\n const expirationOptions = [\n { value: \"30days\", label: \"30 days\" },\n { value: \"90days\", label: \"90 days\" },\n { value: \"180days\", label: \"180 days\" },\n { value: \"1year\", label: \"1 year\" },\n { value: \"2years\", label: \"2 years\" },\n { value: \"never\", label: \"Never expires\" },\n ];\n\n if (newApiKey) {\n return (\n <PageLayout\n title=\"API Key Generated\"\n description=\"Your new API key has been generated successfully\"\n >\n <div className=\"max-w-2xl space-y-6\">\n <NewSecretAlert\n title=\"API Key & Secret\"\n secret={newApiKey.apiSecret}\n description=\"Store this secret securely — you won't be able to see it again.\"\n onDismiss={handleKeyDismiss}\n />\n\n <Alert variant=\"info\" title=\"Usage Instructions\">\n <div className=\"mt-2 space-y-2\">\n <p>Your API key details:</p>\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n Key ID:\n </span>\n <code className=\"rounded bg-muted px-2 py-0.5 text-sm\">\n {newApiKey.keyId}\n </code>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs font-medium text-muted-foreground\">\n API Key:\n </span>\n <code className=\"rounded bg-muted px-2 py-0.5 text-sm\">\n {newApiKey.apiKey}\n </code>\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground mt-2\">\n Use the API key and secret to authenticate your requests.\n </p>\n </div>\n </Alert>\n\n <div className=\"flex gap-3\">\n <button\n onClick={handleKeyDismiss}\n className=\"inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors\"\n >\n Continue to API Keys\n </button>\n </div>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.APP_UPDATE}>\n <PageLayout\n title=\"Generate API Key\"\n description=\"Generate a new API key for programmatic access\"\n backLink=\"/api-keys\"\n backLabel=\"Back to API Keys\"\n >\n <form onSubmit={handleSubmit} className=\"max-w-3xl space-y-8\">\n {/* Mutation error */}\n {mutationError && (\n <Alert\n variant=\"error\"\n title=\"Error Generating API Key\"\n onDismiss={() => setMutationError(null)}\n >\n {mutationError}\n </Alert>\n )}\n\n {/* Basic Information */}\n <PageSection\n title=\"Basic Information\"\n description=\"Configure the API key settings\"\n >\n <div className=\"space-y-4\">\n {/* App Selector */}\n {appsLoading ? (\n <div className=\"flex items-center gap-2 py-2\">\n <Loader2 className=\"h-4 w-4 animate-spin text-muted-foreground\" />\n <span className=\"text-sm text-muted-foreground\">\n Loading apps...\n </span>\n </div>\n ) : apps.length === 0 ? (\n <Alert variant=\"warning\" title=\"No Apps Found\">\n <p>\n You need to create an OAuth app first before generating API\n keys.{\" \"}\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/oauth/new\")}\n className=\"text-primary underline hover:no-underline\"\n >\n Create an app\n </button>\n </p>\n </Alert>\n ) : (\n <SelectField\n label=\"App\"\n value={formData.appId}\n onChange={(e) => updateField(\"appId\", e.target.value)}\n options={[\n { value: \"\", label: \"Select an app...\" },\n ...apps.map((app) => ({\n value: app.id,\n label: `${app.name} (${app.clientId})`,\n })),\n ]}\n error={errors.appId}\n required\n hint=\"The OAuth app this API key will be associated with\"\n />\n )}\n\n <InputField\n label=\"Key Name\"\n placeholder=\"e.g., Production Backend Key\"\n value={formData.name}\n onChange={(e) => updateField(\"name\", e.target.value)}\n error={errors.name}\n required\n hint=\"A descriptive name to identify this key\"\n />\n </div>\n </PageSection>\n\n {/* Scopes */}\n <PageSection\n title=\"Scopes\"\n description=\"Define what this API key can access\"\n >\n <div className=\"space-y-4\">\n <TagInput\n label=\"Scopes\"\n value={formData.scopes}\n onChange={(values) => updateField(\"scopes\", values)}\n placeholder=\"e.g., workspace:read, vibecontrols:write\"\n hint=\"Press Enter to add each scope. At least one scope is required.\"\n error={errors.scopes}\n />\n </div>\n </PageSection>\n\n {/* Advanced: rate-limit overrides / allowed origins / scope tuning / expiration — collapse the long-tail knobs */}\n <OptionalFields\n title=\"Advanced settings\"\n description=\"Rate-limit overrides, allowed origins, scope tuning, and expiration.\"\n >\n <PageSection\n title=\"Key Expiration\"\n description=\"Set when this key should expire\"\n >\n <SelectField\n label=\"Expires In\"\n value={formData.expiresIn}\n onChange={(e) => updateField(\"expiresIn\", e.target.value)}\n options={expirationOptions}\n hint=\"Keys can be revoked at any time regardless of expiration\"\n />\n\n {formData.expiresIn === \"never\" && (\n <Alert variant=\"warning\" className=\"mt-4\">\n Non-expiring keys are a security risk. Consider setting an\n expiration and rotating keys periodically.\n </Alert>\n )}\n </PageSection>\n </OptionalFields>\n\n {/* Actions */}\n <div className=\"flex items-center justify-end gap-3 border-t pt-6\">\n <button\n type=\"button\"\n onClick={handleCancel}\n className=\"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors\"\n >\n <X size={16} />\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting || apps.length === 0}\n className=\"inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50\"\n >\n <Save size={16} />\n {isSubmitting ? \"Generating...\" : \"Generate API Key\"}\n </button>\n </div>\n </form>\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}