@burdenoff/microfe-devportal 2026.528.1 → 2026.530.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.
@@ -18,7 +18,9 @@ var o = ({ title: o, description: s, backLink: c, backLabel: l, actions: u, chil
18
18
  children: [
19
19
  c && /* @__PURE__ */ a("button", {
20
20
  onClick: () => {
21
- !m || !c || m(`${h?.endsWith("/") ? h.slice(0, -1) : h || ""}${c.startsWith("/") ? c : `/${c}`}`);
21
+ if (!m || !c) return;
22
+ let e = h?.endsWith("/") ? h.slice(0, -1) : h || "", t = c.startsWith("/") ? c : `/${c}`;
23
+ m(e.length > 0 && (t === e || t.startsWith(`${e}/`)) ? t : `${e}${t}`);
22
24
  },
23
25
  className: "mb-2 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors",
24
26
  children: [/* @__PURE__ */ i(r, { size: 16 }), _]
@@ -41,8 +43,9 @@ var o = ({ title: o, description: s, backLink: c, backLabel: l, actions: u, chil
41
43
  children: d
42
44
  })]
43
45
  });
44
- }, s = ({ title: e, description: n, actions: r, children: o, className: s }) => /* @__PURE__ */ a("section", {
46
+ }, s = ({ title: e, description: n, actions: r, children: o, className: s, ...c }) => /* @__PURE__ */ a("section", {
45
47
  className: t("space-y-4", s),
48
+ ...c,
46
49
  children: [(e || r) && /* @__PURE__ */ a("div", {
47
50
  className: "flex items-center justify-between",
48
51
  children: [/* @__PURE__ */ a("div", { children: [e && /* @__PURE__ */ i("h2", {
@@ -1 +1 @@
1
- {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/devportal/components/PageLayout.tsx"],"sourcesContent":["import type { FC, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { cn } from \"../utils/cn\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\n\ninterface PageLayoutProps {\n title: string;\n description?: string;\n backLink?: string;\n backLabel?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n fullWidth?: boolean;\n}\n\n/**\n * Standard page layout with header, optional back link, and actions\n */\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n backLink,\n backLabel,\n actions,\n children,\n className,\n fullWidth = false,\n}) => {\n const { navigate, basePath } = useDevportal();\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 resolvedBackLabel = backLabel ?? tr(\"devportal.back\", \"Back\");\n\n const handleBack = () => {\n if (!navigate || !backLink) return;\n\n const normalizedBasePath = basePath?.endsWith(\"/\")\n ? basePath.slice(0, -1)\n : basePath || \"\";\n const normalizedBackLink = backLink.startsWith(\"/\")\n ? backLink\n : `/${backLink}`;\n\n navigate(`${normalizedBasePath}${normalizedBackLink}`);\n };\n\n return (\n <div className={cn(\"flex flex-col gap-6 p-6\", className)}>\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-col gap-1\">\n {backLink && (\n <button\n onClick={handleBack}\n className=\"mb-2 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors\"\n >\n <ArrowLeft size={16} />\n {resolvedBackLabel}\n </button>\n )}\n <h1 className=\"text-2xl font-bold tracking-tight\">{title}</h1>\n {description && (\n <p className=\"text-muted-foreground\">{description}</p>\n )}\n </div>\n {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n </div>\n\n {/* Content */}\n <div className={cn(!fullWidth && \"max-w-7xl\")}>{children}</div>\n </div>\n );\n};\n\n/**\n * Section within a page\n */\nexport const PageSection: FC<{\n title?: string;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n}> = ({ title, description, actions, children, className }) => (\n <section className={cn(\"space-y-4\", className)}>\n {(title || actions) && (\n <div className=\"flex items-center justify-between\">\n <div>\n {title && <h2 className=\"text-lg font-semibold\">{title}</h2>}\n {description && (\n <p className=\"text-sm text-muted-foreground\">{description}</p>\n )}\n </div>\n {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n </div>\n )}\n {children}\n </section>\n);\n"],"mappings":";;;;;;AAoBA,IAAa,KAAmC,EAC9C,UACA,gBACA,aACA,cACA,YACA,aACA,cACA,eAAY,SACR;CACJ,IAAM,EAAE,aAAU,gBAAa,GAAc,EACvC,EAAE,SAAM,GAAS,EAKjB,IAAoB,OAJd,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAEM,kBAAkB,OAAO;AAenE,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,2BAA2B,EAAU;YAAxD,CAEE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,KACC,kBAAC,UAAD;MACE,eApBa;AACnB,QAAC,KAAY,CAAC,KASlB,EAAS,GAPkB,GAAU,SAAS,IAAI,GAC9C,EAAS,MAAM,GAAG,GAAG,GACrB,KAAY,KACW,EAAS,WAAW,IAAI,GAC/C,IACA,IAAI,MAE8C;;MAW5C,WAAU;gBAFZ,CAIE,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA,EACtB,EACM;;KAEX,kBAAC,MAAD;MAAI,WAAU;gBAAqC;MAAW,CAAA;KAC7D,KACC,kBAAC,KAAD;MAAG,WAAU;gBAAyB;MAAgB,CAAA;KAEpD;OACL,KAAW,kBAAC,OAAD;IAAK,WAAU;cAA2B;IAAc,CAAA,CAChE;MAGN,kBAAC,OAAD;GAAK,WAAW,EAAG,CAAC,KAAa,YAAY;GAAG;GAAe,CAAA,CAC3D;;GAOG,KAMP,EAAE,UAAO,gBAAa,YAAS,aAAU,mBAC7C,kBAAC,WAAD;CAAS,WAAW,EAAG,aAAa,EAAU;WAA9C,EACI,KAAS,MACT,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACG,KAAS,kBAAC,MAAD;GAAI,WAAU;aAAyB;GAAW,CAAA,EAC3D,KACC,kBAAC,KAAD;GAAG,WAAU;aAAiC;GAAgB,CAAA,CAE5D,EAAA,CAAA,EACL,KAAW,kBAAC,OAAD;GAAK,WAAU;aAA2B;GAAc,CAAA,CAChE;KAEP,EACO"}
1
+ {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/devportal/components/PageLayout.tsx"],"sourcesContent":["import type { FC, HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport { cn } from \"../utils/cn\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\n\ninterface PageLayoutProps {\n title: string;\n description?: string;\n backLink?: string;\n backLabel?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n fullWidth?: boolean;\n}\n\n/**\n * Standard page layout with header, optional back link, and actions\n */\nexport const PageLayout: FC<PageLayoutProps> = ({\n title,\n description,\n backLink,\n backLabel,\n actions,\n children,\n className,\n fullWidth = false,\n}) => {\n const { navigate, basePath } = useDevportal();\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 resolvedBackLabel = backLabel ?? tr(\"devportal.back\", \"Back\");\n\n const handleBack = () => {\n if (!navigate || !backLink) return;\n\n const normalizedBasePath = basePath?.endsWith(\"/\")\n ? basePath.slice(0, -1)\n : basePath || \"\";\n const normalizedBackLink = backLink.startsWith(\"/\")\n ? backLink\n : `/${backLink}`;\n const alreadyIncludesBasePath =\n normalizedBasePath.length > 0 &&\n (normalizedBackLink === normalizedBasePath ||\n normalizedBackLink.startsWith(`${normalizedBasePath}/`));\n\n navigate(\n alreadyIncludesBasePath\n ? normalizedBackLink\n : `${normalizedBasePath}${normalizedBackLink}`,\n );\n };\n\n return (\n <div className={cn(\"flex flex-col gap-6 p-6\", className)}>\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-col gap-1\">\n {backLink && (\n <button\n onClick={handleBack}\n className=\"mb-2 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors\"\n >\n <ArrowLeft size={16} />\n {resolvedBackLabel}\n </button>\n )}\n <h1 className=\"text-2xl font-bold tracking-tight\">{title}</h1>\n {description && (\n <p className=\"text-muted-foreground\">{description}</p>\n )}\n </div>\n {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n </div>\n\n {/* Content */}\n <div className={cn(!fullWidth && \"max-w-7xl\")}>{children}</div>\n </div>\n );\n};\n\n/**\n * Section within a page\n */\nexport const PageSection: FC<\n {\n title?: string;\n description?: string;\n actions?: ReactNode;\n children: ReactNode;\n className?: string;\n } & HTMLAttributes<HTMLElement>\n> = ({ title, description, actions, children, className, ...rest }) => (\n <section className={cn(\"space-y-4\", className)} {...rest}>\n {(title || actions) && (\n <div className=\"flex items-center justify-between\">\n <div>\n {title && <h2 className=\"text-lg font-semibold\">{title}</h2>}\n {description && (\n <p className=\"text-sm text-muted-foreground\">{description}</p>\n )}\n </div>\n {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n </div>\n )}\n {children}\n </section>\n);\n"],"mappings":";;;;;;AAoBA,IAAa,KAAmC,EAC9C,UACA,gBACA,aACA,cACA,YACA,aACA,cACA,eAAY,SACR;CACJ,IAAM,EAAE,aAAU,gBAAa,GAAc,EACvC,EAAE,SAAM,GAAS,EAKjB,IAAoB,OAJd,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAEM,kBAAkB,OAAO;AAuBnE,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,2BAA2B,EAAU;YAAxD,CAEE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,KACC,kBAAC,UAAD;MACE,eA5Ba;AACvB,WAAI,CAAC,KAAY,CAAC,EAAU;OAE5B,IAAM,IAAqB,GAAU,SAAS,IAAI,GAC9C,EAAS,MAAM,GAAG,GAAG,GACrB,KAAY,IACV,IAAqB,EAAS,WAAW,IAAI,GAC/C,IACA,IAAI;AAMR,SAJE,EAAmB,SAAS,MAC3B,MAAuB,KACtB,EAAmB,WAAW,GAAG,EAAmB,GAAG,IAIrD,IACA,GAAG,IAAqB,IAC7B;;MAWS,WAAU;gBAFZ,CAIE,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA,EACtB,EACM;;KAEX,kBAAC,MAAD;MAAI,WAAU;gBAAqC;MAAW,CAAA;KAC7D,KACC,kBAAC,KAAD;MAAG,WAAU;gBAAyB;MAAgB,CAAA;KAEpD;OACL,KAAW,kBAAC,OAAD;IAAK,WAAU;cAA2B;IAAc,CAAA,CAChE;MAGN,kBAAC,OAAD;GAAK,WAAW,EAAG,CAAC,KAAa,YAAY;GAAG;GAAe,CAAA,CAC3D;;GAOG,KAQR,EAAE,UAAO,gBAAa,YAAS,aAAU,cAAW,GAAG,QAC1D,kBAAC,WAAD;CAAS,WAAW,EAAG,aAAa,EAAU;CAAE,GAAI;WAApD,EACI,KAAS,MACT,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACG,KAAS,kBAAC,MAAD;GAAI,WAAU;aAAyB;GAAW,CAAA,EAC3D,KACC,kBAAC,KAAD;GAAG,WAAU;aAAiC;GAAgB,CAAA,CAE5D,EAAA,CAAA,EACL,KAAW,kBAAC,OAAD;GAAK,WAAU;aAA2B;GAAc,CAAA,CAChE;KAEP,EACO"}
@@ -1,8 +1,9 @@
1
1
  import { useDevportalEventBus as e } from "../events/index.js";
2
2
  import { Component as t } from "react";
3
- import { jsx as n, jsxs as r } from "react/jsx-runtime";
3
+ import { useInRouterContext as n, useLocation as r } from "react-router-dom";
4
+ import { jsx as i, jsxs as a } from "react/jsx-runtime";
4
5
  //#region src/devportal/components/TelemetryErrorBoundary.tsx
5
- var i = class extends t {
6
+ var o = class extends t {
6
7
  state = { hasError: !1 };
7
8
  static getDerivedStateFromError(e) {
8
9
  return {
@@ -13,37 +14,56 @@ var i = class extends t {
13
14
  componentDidCatch(e, t) {
14
15
  this.props.emit(e.message, e.stack, t.componentStack ?? void 0);
15
16
  }
17
+ componentDidUpdate(e) {
18
+ this.state.hasError && e.locationKey !== this.props.locationKey && this.setState({
19
+ hasError: !1,
20
+ message: void 0
21
+ });
22
+ }
16
23
  render() {
17
- return this.state.hasError ? this.props.fallback ?? /* @__PURE__ */ r("div", {
24
+ return this.state.hasError ? this.props.fallback ?? /* @__PURE__ */ a("div", {
18
25
  role: "alert",
19
26
  className: "rounded-lg border border-border bg-bg-secondary p-6 text-text-primary",
20
- children: [/* @__PURE__ */ n("h2", {
27
+ children: [/* @__PURE__ */ i("h2", {
21
28
  className: "text-lg font-semibold",
22
29
  children: "Something went wrong"
23
- }), /* @__PURE__ */ n("p", {
30
+ }), /* @__PURE__ */ i("p", {
24
31
  className: "mt-2 text-sm text-text-secondary",
25
32
  children: "The page failed to render. Try refreshing — our team has been notified."
26
33
  })]
27
34
  }) : this.props.children;
28
35
  }
29
36
  };
30
- function a({ children: t, fallback: r, scope: a }) {
31
- let o = e();
32
- return /* @__PURE__ */ n(i, {
37
+ function s(e) {
38
+ let t = r();
39
+ return /* @__PURE__ */ i(o, {
40
+ ...e,
41
+ locationKey: t.pathname
42
+ });
43
+ }
44
+ function c({ children: t, fallback: r, scope: a }) {
45
+ let c = e(), l = n(), u = (e, t, n) => {
46
+ c.emit("devportal.error.boundary", {
47
+ scope: a ?? "unknown",
48
+ message: e,
49
+ stack: t,
50
+ componentStack: n
51
+ });
52
+ };
53
+ return l ? /* @__PURE__ */ i(s, {
54
+ fallback: r,
55
+ scope: a,
56
+ emit: u,
57
+ children: t
58
+ }) : /* @__PURE__ */ i(o, {
33
59
  fallback: r,
34
60
  scope: a,
35
- emit: (e, t, n) => {
36
- o.emit("devportal.error.boundary", {
37
- scope: a ?? "unknown",
38
- message: e,
39
- stack: t,
40
- componentStack: n
41
- });
42
- },
61
+ emit: u,
62
+ locationKey: "",
43
63
  children: t
44
64
  });
45
65
  }
46
66
  //#endregion
47
- export { a as TelemetryErrorBoundary };
67
+ export { c as TelemetryErrorBoundary };
48
68
 
49
69
  //# sourceMappingURL=TelemetryErrorBoundary.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TelemetryErrorBoundary.js","names":[],"sources":["../../../src/devportal/components/TelemetryErrorBoundary.tsx"],"sourcesContent":["/**\n * TelemetryErrorBoundary\n *\n * React error boundary that emits a `devportal.error.boundary` event to the\n * frontend event bus when a render error occurs. The app shell's RybbitSink /\n * BackendAuditSink / OtelBrowserSink consume the event — MFEs never call\n * Rybbit or analytics endpoints directly (memory rule + spec 47).\n *\n * If the EventBusProvider isn't mounted (standalone dev mode), the boundary\n * still catches errors and renders the fallback UI; emission is just skipped.\n */\nimport { Component, type ErrorInfo, type ReactNode } from \"react\";\nimport { useDevportalEventBus } from \"../events\";\n\ninterface Props {\n children: ReactNode;\n fallback?: ReactNode;\n /** Optional logical name (page slug) included in event payload. */\n scope?: string;\n}\n\ninterface State {\n hasError: boolean;\n message?: string;\n}\n\nclass InnerBoundary extends Component<\n Props & {\n emit: (\n message: string,\n stack: string | undefined,\n componentStack: string | undefined,\n ) => void;\n },\n State\n> {\n state: State = { hasError: false };\n\n static getDerivedStateFromError(error: Error): State {\n return { hasError: true, message: error.message };\n }\n\n componentDidCatch(error: Error, info: ErrorInfo): void {\n this.props.emit(\n error.message,\n error.stack,\n info.componentStack ?? undefined,\n );\n }\n\n render(): ReactNode {\n if (this.state.hasError) {\n return (\n this.props.fallback ?? (\n <div\n role=\"alert\"\n className=\"rounded-lg border border-border bg-bg-secondary p-6 text-text-primary\"\n >\n <h2 className=\"text-lg font-semibold\">Something went wrong</h2>\n <p className=\"mt-2 text-sm text-text-secondary\">\n The page failed to render. Try refreshing — our team has been\n notified.\n </p>\n </div>\n )\n );\n }\n return this.props.children;\n }\n}\n\nexport function TelemetryErrorBoundary({\n children,\n fallback,\n scope,\n}: Props): ReactNode {\n const bus = useDevportalEventBus();\n const emit = (\n message: string,\n stack: string | undefined,\n componentStack: string | undefined,\n ): void => {\n bus.emit(\"devportal.error.boundary\", {\n scope: scope ?? \"unknown\",\n message,\n stack,\n componentStack,\n });\n };\n\n return (\n <InnerBoundary fallback={fallback} scope={scope} emit={emit}>\n {children}\n </InnerBoundary>\n );\n}\n"],"mappings":";;;;AA0BA,IAAM,IAAN,cAA4B,EAS1B;CACA,QAAe,EAAE,UAAU,IAAO;CAElC,OAAO,yBAAyB,GAAqB;AACnD,SAAO;GAAE,UAAU;GAAM,SAAS,EAAM;GAAS;;CAGnD,kBAAkB,GAAc,GAAuB;AACrD,OAAK,MAAM,KACT,EAAM,SACN,EAAM,OACN,EAAK,kBAAkB,KAAA,EACxB;;CAGH,SAAoB;AAiBlB,SAhBI,KAAK,MAAM,WAEX,KAAK,MAAM,YACT,kBAAC,OAAD;GACE,MAAK;GACL,WAAU;aAFZ,CAIE,kBAAC,MAAD;IAAI,WAAU;cAAwB;IAAyB,CAAA,EAC/D,kBAAC,KAAD;IAAG,WAAU;cAAmC;IAG5C,CAAA,CACA;OAIL,KAAK,MAAM;;;AAItB,SAAgB,EAAuB,EACrC,aACA,aACA,YACmB;CACnB,IAAM,IAAM,GAAsB;AAclC,QACE,kBAAC,GAAD;EAAyB;EAAiB;EAAa,OAbvD,GACA,GACA,MACS;AACT,KAAI,KAAK,4BAA4B;IACnC,OAAO,KAAS;IAChB;IACA;IACA;IACD,CAAC;;EAKC;EACa,CAAA"}
1
+ {"version":3,"file":"TelemetryErrorBoundary.js","names":[],"sources":["../../../src/devportal/components/TelemetryErrorBoundary.tsx"],"sourcesContent":["/**\n * TelemetryErrorBoundary\n *\n * React error boundary that emits a `devportal.error.boundary` event to the\n * frontend event bus when a render error occurs. The app shell's RybbitSink /\n * BackendAuditSink / OtelBrowserSink consume the event — MFEs never call\n * Rybbit or analytics endpoints directly (memory rule + spec 47).\n *\n * If the EventBusProvider isn't mounted (standalone dev mode), the boundary\n * still catches errors and renders the fallback UI; emission is just skipped.\n */\nimport { Component, type ErrorInfo, type ReactNode } from \"react\";\nimport { useInRouterContext, useLocation } from \"react-router-dom\";\nimport { useDevportalEventBus } from \"../events\";\n\ninterface Props {\n children: ReactNode;\n fallback?: ReactNode;\n /** Optional logical name (page slug) included in event payload. */\n scope?: string;\n}\n\ninterface State {\n hasError: boolean;\n message?: string;\n}\n\ntype EmitFn = (\n message: string,\n stack: string | undefined,\n componentStack: string | undefined,\n) => void;\n\ninterface InnerProps extends Props {\n /**\n * Changes on every route navigation. When it differs from the previous\n * render the boundary clears any caught error so a crash on one page does\n * not keep the fallback pinned across subsequent navigations.\n */\n locationKey: string;\n emit: EmitFn;\n}\n\nclass InnerBoundary extends Component<InnerProps, State> {\n state: State = { hasError: false };\n\n static getDerivedStateFromError(error: Error): State {\n return { hasError: true, message: error.message };\n }\n\n componentDidCatch(error: Error, info: ErrorInfo): void {\n this.props.emit(\n error.message,\n error.stack,\n info.componentStack ?? undefined,\n );\n }\n\n componentDidUpdate(prevProps: Readonly<InnerProps>): void {\n if (\n this.state.hasError &&\n prevProps.locationKey !== this.props.locationKey\n ) {\n this.setState({ hasError: false, message: undefined });\n }\n }\n\n render(): ReactNode {\n if (this.state.hasError) {\n return (\n this.props.fallback ?? (\n <div\n role=\"alert\"\n className=\"rounded-lg border border-border bg-bg-secondary p-6 text-text-primary\"\n >\n <h2 className=\"text-lg font-semibold\">Something went wrong</h2>\n <p className=\"mt-2 text-sm text-text-secondary\">\n The page failed to render. Try refreshing — our team has been\n notified.\n </p>\n </div>\n )\n );\n }\n return this.props.children;\n }\n}\n\n/**\n * Reads the current route so the boundary resets on navigation. Rendered only\n * when a Router is present — `useLocation()` throws outside router context, so\n * standalone usage (and the component's own unit tests, which render it without\n * a Router) must never reach this path.\n */\nfunction RoutedBoundary(props: Omit<InnerProps, \"locationKey\">): ReactNode {\n const location = useLocation();\n return <InnerBoundary {...props} locationKey={location.pathname} />;\n}\n\nexport function TelemetryErrorBoundary({\n children,\n fallback,\n scope,\n}: Props): ReactNode {\n const bus = useDevportalEventBus();\n const inRouter = useInRouterContext();\n const emit: EmitFn = (message, stack, componentStack): void => {\n bus.emit(\"devportal.error.boundary\", {\n scope: scope ?? \"unknown\",\n message,\n stack,\n componentStack,\n });\n };\n\n // Inside a router, derive the reset key from the current path so a crash on\n // one page doesn't pin the fallback across navigations. Outside a router\n // (standalone usage / unit tests) use a static key — there is nothing to\n // navigate, and calling useLocation() there would throw.\n if (inRouter) {\n return (\n <RoutedBoundary fallback={fallback} scope={scope} emit={emit}>\n {children}\n </RoutedBoundary>\n );\n }\n\n return (\n <InnerBoundary fallback={fallback} scope={scope} emit={emit} locationKey=\"\">\n {children}\n </InnerBoundary>\n );\n}\n"],"mappings":";;;;;AA2CA,IAAM,IAAN,cAA4B,EAA6B;CACvD,QAAe,EAAE,UAAU,IAAO;CAElC,OAAO,yBAAyB,GAAqB;AACnD,SAAO;GAAE,UAAU;GAAM,SAAS,EAAM;GAAS;;CAGnD,kBAAkB,GAAc,GAAuB;AACrD,OAAK,MAAM,KACT,EAAM,SACN,EAAM,OACN,EAAK,kBAAkB,KAAA,EACxB;;CAGH,mBAAmB,GAAuC;AACxD,EACE,KAAK,MAAM,YACX,EAAU,gBAAgB,KAAK,MAAM,eAErC,KAAK,SAAS;GAAE,UAAU;GAAO,SAAS,KAAA;GAAW,CAAC;;CAI1D,SAAoB;AAiBlB,SAhBI,KAAK,MAAM,WAEX,KAAK,MAAM,YACT,kBAAC,OAAD;GACE,MAAK;GACL,WAAU;aAFZ,CAIE,kBAAC,MAAD;IAAI,WAAU;cAAwB;IAAyB,CAAA,EAC/D,kBAAC,KAAD;IAAG,WAAU;cAAmC;IAG5C,CAAA,CACA;OAIL,KAAK,MAAM;;;AAUtB,SAAS,EAAe,GAAmD;CACzE,IAAM,IAAW,GAAa;AAC9B,QAAO,kBAAC,GAAD;EAAe,GAAI;EAAO,aAAa,EAAS;EAAY,CAAA;;AAGrE,SAAgB,EAAuB,EACrC,aACA,aACA,YACmB;CACnB,IAAM,IAAM,GAAsB,EAC5B,IAAW,GAAoB,EAC/B,KAAgB,GAAS,GAAO,MAAyB;AAC7D,IAAI,KAAK,4BAA4B;GACnC,OAAO,KAAS;GAChB;GACA;GACA;GACD,CAAC;;AAeJ,QARI,IAEA,kBAAC,GAAD;EAA0B;EAAiB;EAAa;EACrD;EACc,CAAA,GAKnB,kBAAC,GAAD;EAAyB;EAAiB;EAAa;EAAM,aAAY;EACtE;EACa,CAAA"}
@@ -174,15 +174,8 @@ var f = !1, p = f ? "/devportal-graphql" : "http://localhost:4015/graphql", m =
174
174
  OAuthClient: { keyFields: ["id"] }
175
175
  } }),
176
176
  defaultOptions: {
177
- watchQuery: {
178
- fetchPolicy: "cache-and-network",
179
- errorPolicy: "all"
180
- },
181
- query: {
182
- fetchPolicy: "cache-first",
183
- errorPolicy: "all"
184
- },
185
- mutate: { errorPolicy: "all" }
177
+ watchQuery: { fetchPolicy: "cache-and-network" },
178
+ query: { fetchPolicy: "cache-first" }
186
179
  }
187
180
  });
188
181
  function S(n) {
@@ -1 +1 @@
1
- {"version":3,"file":"useGraphQL.js","names":[],"sources":["../../../src/devportal/hooks/useGraphQL.ts"],"sourcesContent":["/**\n * Apollo Client hooks and utilities for DevPortal\n *\n * This file provides the GraphQL client setup and custom hooks for the Developer Portal.\n * It wraps the generated Apollo hooks with additional functionality like error handling,\n * loading states, and authentication context.\n */\n\nimport {\n ApolloClient,\n InMemoryCache,\n createHttpLink,\n from,\n split,\n} from \"@apollo/client/core\";\nimport { CombinedGraphQLErrors, ServerError } from \"@apollo/client/errors\";\nimport { setContext } from \"@apollo/client/link/context\";\nimport { onError } from \"@apollo/client/link/error\";\nimport {\n isAuthNetworkError,\n redirectToLogin,\n} from \"@burdenoff/fe-libs/shared/utils\";\n\nfunction hasSessionAuthErrors(\n errors: ReadonlyArray<{\n message?: string;\n extensions?: Record<string, unknown>;\n }> = [],\n): boolean {\n return errors.some((error) => {\n const code =\n typeof error.extensions?.code === \"string\" ? error.extensions.code : \"\";\n const errorCode =\n typeof error.extensions?.errorCode === \"string\"\n ? error.extensions.errorCode\n : \"\";\n const message = (error.message ?? \"\").toLowerCase();\n\n return (\n code === \"UNAUTHENTICATED\" ||\n errorCode === \"TOKEN_EXPIRED\" ||\n message.includes(\"token_expired\") ||\n message.includes(\"token expired\") ||\n message.includes(\"session expired\") ||\n message.includes(\"unauthorized\") ||\n message.includes(\"unauthenticated\") ||\n message.includes(\"authentication required\") ||\n message.includes(\"workspace context missing\")\n );\n });\n}\n\n// Re-export all generated hooks for direct use\nexport * from \"../../generated/global-operations\";\n\n// Vite environment variable declaration\ndeclare global {\n interface ImportMeta {\n readonly env: {\n readonly VITE_GRAPHQL_ENDPOINT?: string;\n readonly VITE_AUTH_GRAPHQL_ENDPOINT?: string;\n readonly VITE_DEV_ACTOR_ID?: string;\n readonly VITE_DEV_ACTOR_TYPE?: string;\n readonly DEV?: boolean;\n [key: string]: string | boolean | undefined;\n };\n }\n}\n\n// Development mode check\nconst isDevelopment =\n typeof import.meta !== \"undefined\" && import.meta.env?.DEV === true;\n\n// Default GraphQL endpoints - can be overridden via environment.\n// In standalone dev mode we route through same-origin Vite proxies to avoid CORS.\nconst DEFAULT_GRAPHQL_ENDPOINT =\n (typeof import.meta !== \"undefined\" &&\n import.meta.env?.VITE_GRAPHQL_ENDPOINT) ||\n (isDevelopment ? \"/devportal-graphql\" : \"http://localhost:4015/graphql\");\n\nconst DEFAULT_AUTH_GRAPHQL_ENDPOINT =\n (typeof import.meta !== \"undefined\" &&\n import.meta.env?.VITE_AUTH_GRAPHQL_ENDPOINT) ||\n (isDevelopment ? \"/auth-graphql\" : \"http://localhost:4011/graphql\");\n\nconst AUTH_OPERATION_NAMES = new Set([\n \"CurrentUserApps\",\n \"GetAuthApp\",\n \"AppsActingAsCurrentUser\",\n \"CreateAuthApp\",\n \"UpdateAuthApp\",\n \"DeactivateApp\",\n \"RotateAppSecret\",\n \"AuthenticateApp\",\n \"GenerateAppToken\",\n \"GenerateAppAsUserToken\",\n \"ExchangeToken\",\n \"CurrentUserDelegations\",\n \"CurrentUserGrantedDelegations\",\n \"GrantDelegation\",\n \"RevokeDelegation\",\n \"MyApiKeys\",\n \"GenerateAPIKey\",\n \"RevokeAPIKeyAuth\",\n \"RotateAPIKeyAuth\",\n \"AuthenticateAPIKey\",\n]);\n\n// Development actor ID - simulates gateway-injected headers\n// In production, the gateway adds these after JWT verification\nconst DEV_ACTOR_ID =\n (typeof import.meta !== \"undefined\" && import.meta.env?.VITE_DEV_ACTOR_ID) ||\n \"dev-user-001\";\n\nconst DEV_ACTOR_TYPE =\n (typeof import.meta !== \"undefined\" &&\n import.meta.env?.VITE_DEV_ACTOR_TYPE) ||\n \"user\";\n\n// Create the HTTP links\nconst devportalHttpLink = createHttpLink({\n uri: DEFAULT_GRAPHQL_ENDPOINT,\n});\n\nconst authHttpLink = createHttpLink({\n uri: DEFAULT_AUTH_GRAPHQL_ENDPOINT,\n});\n\nconst httpLink = split(\n ({ operationName }) => {\n if (!operationName) {\n return false;\n }\n\n return AUTH_OPERATION_NAMES.has(operationName);\n },\n authHttpLink,\n devportalHttpLink,\n);\n\n// Auth link to add authentication headers\nconst authLink = setContext((_, { headers }) => {\n // Get auth token from storage or context\n const token =\n typeof window !== \"undefined\" ? localStorage.getItem(\"auth_token\") : null;\n\n // Build headers\n const authHeaders: Record<string, string> = {\n ...headers,\n };\n\n if (token) {\n authHeaders[\"authorization\"] = `Bearer ${token}`;\n }\n\n // In development mode, add actor headers to simulate gateway behavior\n // In production, the API Gateway adds these headers after JWT verification\n if (isDevelopment) {\n // Use actor ID from localStorage (set by dev tools) or fall back to env/default\n const actorId =\n (typeof window !== \"undefined\" && localStorage.getItem(\"dev_actor_id\")) ||\n DEV_ACTOR_ID;\n const actorType =\n (typeof window !== \"undefined\" &&\n localStorage.getItem(\"dev_actor_type\")) ||\n DEV_ACTOR_TYPE;\n\n authHeaders[\"x-actor-id\"] = actorId;\n authHeaders[\"x-actor-type\"] = actorType;\n }\n\n return { headers: authHeaders };\n});\n\nconst errorLink = onError(({ error }) => {\n if (!error) return;\n\n const graphQLErrors =\n \"errors\" in error && Array.isArray(error.errors) ? error.errors : [];\n\n if (hasSessionAuthErrors(graphQLErrors)) {\n redirectToLogin();\n return;\n }\n\n if (error instanceof Error && isAuthNetworkError(error)) {\n redirectToLogin();\n }\n});\n\n// Create the Apollo Client instance\nexport const apolloClient = new ApolloClient({\n link: from([errorLink, authLink, httpLink]),\n cache: new InMemoryCache({\n typePolicies: {\n Query: {\n fields: {\n // Pagination policies for connection types\n developerOrganizations: {\n keyArgs: [\"tier\", \"isVerified\", \"isCertified\", \"partnerTier\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) {\n return incoming;\n }\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n applications: {\n keyArgs: [\"status\", \"visibility\", \"developerId\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) {\n return incoming;\n }\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n developerOrgSales: {\n keyArgs: [\n \"organizationId\",\n \"applicationId\",\n \"status\",\n \"pricingModel\",\n \"isPaidOut\",\n ],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) {\n return incoming;\n }\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n developerOrgPayouts: {\n keyArgs: [\"organizationId\", \"status\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) {\n return incoming;\n }\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n developerOrgMembers: {\n keyArgs: [\"organizationId\", \"role\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n developerOrgInvitations: {\n keyArgs: [\"organizationId\", \"status\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n webhooks: {\n keyArgs: [\"applicationId\", \"status\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n webhookDeliveries: {\n keyArgs: [\"webhookId\", \"status\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n auditLogs: {\n keyArgs: [\"resource\", \"resourceId\", \"actorId\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n applicationVersions: {\n // TODO(phase1-backend): Switch to ApplicationVersionConnection cursor pagination\n // once backend ships connection type. See stability tracker.\n keyArgs: [\"applicationId\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n if (Array.isArray(incoming) && Array.isArray(existing)) {\n return [...existing, ...incoming];\n }\n return incoming;\n },\n },\n },\n },\n WebhookDelivery: {\n keyFields: [\"id\"],\n },\n ApplicationVersion: {\n keyFields: [\"id\"],\n },\n DeveloperOrganization: {\n keyFields: [\"id\"],\n },\n Application: {\n keyFields: [\"id\"],\n },\n DeveloperOrgMember: {\n keyFields: [\"id\"],\n },\n DeveloperOrgInvitation: {\n keyFields: [\"id\"],\n },\n Webhook: {\n keyFields: [\"id\"],\n },\n ApiKey: {\n keyFields: [\"id\"],\n },\n OAuthClient: {\n keyFields: [\"id\"],\n },\n },\n }),\n defaultOptions: {\n watchQuery: {\n fetchPolicy: \"cache-and-network\",\n errorPolicy: \"all\",\n },\n query: {\n fetchPolicy: \"cache-first\",\n errorPolicy: \"all\",\n },\n mutate: {\n errorPolicy: \"all\",\n },\n },\n});\n\n/**\n * Error handling utilities\n */\nexport interface GraphQLErrorInfo {\n message: string;\n code?: string;\n path?: readonly (string | number)[];\n}\n\n// Type for Apollo errors (can be CombinedGraphQLErrors, ServerError, or Error)\nexport type ApolloErrorLike = Error & {\n graphQLErrors?: readonly {\n message: string;\n extensions?: { code?: string };\n path?: readonly (string | number)[];\n }[];\n networkError?: Error | null;\n};\n\nexport function parseGraphQLError(error: ApolloErrorLike): GraphQLErrorInfo[] {\n if (error instanceof CombinedGraphQLErrors) {\n return error.errors.map((err) => ({\n message: err.message,\n code: err.extensions?.code as string | undefined,\n path: err.path,\n }));\n }\n\n if (error.graphQLErrors && error.graphQLErrors.length > 0) {\n return error.graphQLErrors.map((err) => ({\n message: err.message,\n code: err.extensions?.code as string | undefined,\n path: err.path,\n }));\n }\n\n if (error instanceof ServerError || error.networkError) {\n return [\n {\n message: \"Network error. Please check your connection and try again.\",\n code: \"NETWORK_ERROR\",\n },\n ];\n }\n\n return [\n {\n message: error.message || \"An unexpected error occurred\",\n code: \"UNKNOWN_ERROR\",\n },\n ];\n}\n\n/**\n * Get a user-friendly error message from an Apollo error\n */\nexport function getErrorMessage(error: ApolloErrorLike): string {\n const errors = parseGraphQLError(error);\n return errors.map((e) => e.message).join(\", \");\n}\n\n/**\n * Check if an error is an authentication error\n */\nexport function isAuthError(error: ApolloErrorLike): boolean {\n const errors = parseGraphQLError(error);\n return errors.some(\n (e) => e.code === \"UNAUTHENTICATED\" || e.code === \"FORBIDDEN\",\n );\n}\n\n/**\n * Check if an error is a validation error\n */\nexport function isValidationError(error: ApolloErrorLike): boolean {\n const errors = parseGraphQLError(error);\n return errors.some(\n (e) => e.code === \"BAD_USER_INPUT\" || e.code === \"VALIDATION_ERROR\",\n );\n}\n\n/**\n * Clear Apollo cache (useful for logout)\n */\nexport async function clearApolloCache(): Promise<void> {\n await apolloClient.clearStore();\n}\n\n/**\n * Reset Apollo store and refetch queries\n */\nexport async function resetApolloStore(): Promise<void> {\n await apolloClient.resetStore();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useGraphQL.js","names":[],"sources":["../../../src/devportal/hooks/useGraphQL.ts"],"sourcesContent":["/**\n * Apollo Client hooks and utilities for DevPortal\n *\n * This file provides the GraphQL client setup and custom hooks for the Developer Portal.\n * It wraps the generated Apollo hooks with additional functionality like error handling,\n * loading states, and authentication context.\n */\n\nimport {\n ApolloClient,\n InMemoryCache,\n createHttpLink,\n from,\n split,\n} from \"@apollo/client/core\";\nimport { CombinedGraphQLErrors, ServerError } from \"@apollo/client/errors\";\nimport { setContext } from \"@apollo/client/link/context\";\nimport { onError } from \"@apollo/client/link/error\";\nimport {\n isAuthNetworkError,\n redirectToLogin,\n} from \"@burdenoff/fe-libs/shared/utils\";\n\nfunction hasSessionAuthErrors(\n errors: ReadonlyArray<{\n message?: string;\n extensions?: Record<string, unknown>;\n }> = [],\n): boolean {\n return errors.some((error) => {\n const code =\n typeof error.extensions?.code === \"string\" ? error.extensions.code : \"\";\n const errorCode =\n typeof error.extensions?.errorCode === \"string\"\n ? error.extensions.errorCode\n : \"\";\n const message = (error.message ?? \"\").toLowerCase();\n\n return (\n code === \"UNAUTHENTICATED\" ||\n errorCode === \"TOKEN_EXPIRED\" ||\n message.includes(\"token_expired\") ||\n message.includes(\"token expired\") ||\n message.includes(\"session expired\") ||\n message.includes(\"unauthorized\") ||\n message.includes(\"unauthenticated\") ||\n message.includes(\"authentication required\") ||\n message.includes(\"workspace context missing\")\n );\n });\n}\n\n// Re-export all generated hooks for direct use\nexport * from \"../../generated/global-operations\";\n\n// Vite environment variable declaration\ndeclare global {\n interface ImportMeta {\n readonly env: {\n readonly VITE_GRAPHQL_ENDPOINT?: string;\n readonly VITE_AUTH_GRAPHQL_ENDPOINT?: string;\n readonly VITE_DEV_ACTOR_ID?: string;\n readonly VITE_DEV_ACTOR_TYPE?: string;\n readonly DEV?: boolean;\n [key: string]: string | boolean | undefined;\n };\n }\n}\n\n// Development mode check\nconst isDevelopment =\n typeof import.meta !== \"undefined\" && import.meta.env?.DEV === true;\n\n// Default GraphQL endpoints - can be overridden via environment.\n// In standalone dev mode we route through same-origin Vite proxies to avoid CORS.\nconst DEFAULT_GRAPHQL_ENDPOINT =\n (typeof import.meta !== \"undefined\" &&\n import.meta.env?.VITE_GRAPHQL_ENDPOINT) ||\n (isDevelopment ? \"/devportal-graphql\" : \"http://localhost:4015/graphql\");\n\nconst DEFAULT_AUTH_GRAPHQL_ENDPOINT =\n (typeof import.meta !== \"undefined\" &&\n import.meta.env?.VITE_AUTH_GRAPHQL_ENDPOINT) ||\n (isDevelopment ? \"/auth-graphql\" : \"http://localhost:4011/graphql\");\n\nconst AUTH_OPERATION_NAMES = new Set([\n \"CurrentUserApps\",\n \"GetAuthApp\",\n \"AppsActingAsCurrentUser\",\n \"CreateAuthApp\",\n \"UpdateAuthApp\",\n \"DeactivateApp\",\n \"RotateAppSecret\",\n \"AuthenticateApp\",\n \"GenerateAppToken\",\n \"GenerateAppAsUserToken\",\n \"ExchangeToken\",\n \"CurrentUserDelegations\",\n \"CurrentUserGrantedDelegations\",\n \"GrantDelegation\",\n \"RevokeDelegation\",\n \"MyApiKeys\",\n \"GenerateAPIKey\",\n \"RevokeAPIKeyAuth\",\n \"RotateAPIKeyAuth\",\n \"AuthenticateAPIKey\",\n]);\n\n// Development actor ID - simulates gateway-injected headers\n// In production, the gateway adds these after JWT verification\nconst DEV_ACTOR_ID =\n (typeof import.meta !== \"undefined\" && import.meta.env?.VITE_DEV_ACTOR_ID) ||\n \"dev-user-001\";\n\nconst DEV_ACTOR_TYPE =\n (typeof import.meta !== \"undefined\" &&\n import.meta.env?.VITE_DEV_ACTOR_TYPE) ||\n \"user\";\n\n// Create the HTTP links\nconst devportalHttpLink = createHttpLink({\n uri: DEFAULT_GRAPHQL_ENDPOINT,\n});\n\nconst authHttpLink = createHttpLink({\n uri: DEFAULT_AUTH_GRAPHQL_ENDPOINT,\n});\n\nconst httpLink = split(\n ({ operationName }) => {\n if (!operationName) {\n return false;\n }\n\n return AUTH_OPERATION_NAMES.has(operationName);\n },\n authHttpLink,\n devportalHttpLink,\n);\n\n// Auth link to add authentication headers\nconst authLink = setContext((_, { headers }) => {\n // Get auth token from storage or context\n const token =\n typeof window !== \"undefined\" ? localStorage.getItem(\"auth_token\") : null;\n\n // Build headers\n const authHeaders: Record<string, string> = {\n ...headers,\n };\n\n if (token) {\n authHeaders[\"authorization\"] = `Bearer ${token}`;\n }\n\n // In development mode, add actor headers to simulate gateway behavior\n // In production, the API Gateway adds these headers after JWT verification\n if (isDevelopment) {\n // Use actor ID from localStorage (set by dev tools) or fall back to env/default\n const actorId =\n (typeof window !== \"undefined\" && localStorage.getItem(\"dev_actor_id\")) ||\n DEV_ACTOR_ID;\n const actorType =\n (typeof window !== \"undefined\" &&\n localStorage.getItem(\"dev_actor_type\")) ||\n DEV_ACTOR_TYPE;\n\n authHeaders[\"x-actor-id\"] = actorId;\n authHeaders[\"x-actor-type\"] = actorType;\n }\n\n return { headers: authHeaders };\n});\n\nconst errorLink = onError(({ error }) => {\n if (!error) return;\n\n const graphQLErrors =\n \"errors\" in error && Array.isArray(error.errors) ? error.errors : [];\n\n if (hasSessionAuthErrors(graphQLErrors)) {\n redirectToLogin();\n return;\n }\n\n if (error instanceof Error && isAuthNetworkError(error)) {\n redirectToLogin();\n }\n});\n\n// Create the Apollo Client instance\n// NOTE: defaultOptions.errorPolicy is intentionally omitted. Apollo Client v4\n// guards it behind a DeclareDefaultOptions module augmentation that does not\n// resolve reliably in CI's frozen install, so it is left at the v4 default\n// (\"none\"). Per-operation errorPolicy: \"all\" can be set on individual hooks\n// where partial-data-on-error handling is required.\nexport const apolloClient = new ApolloClient({\n link: from([errorLink, authLink, httpLink]),\n cache: new InMemoryCache({\n typePolicies: {\n Query: {\n fields: {\n // Pagination policies for connection types\n developerOrganizations: {\n keyArgs: [\"tier\", \"isVerified\", \"isCertified\", \"partnerTier\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) {\n return incoming;\n }\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n applications: {\n keyArgs: [\"status\", \"visibility\", \"developerId\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) {\n return incoming;\n }\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n developerOrgSales: {\n keyArgs: [\n \"organizationId\",\n \"applicationId\",\n \"status\",\n \"pricingModel\",\n \"isPaidOut\",\n ],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) {\n return incoming;\n }\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n developerOrgPayouts: {\n keyArgs: [\"organizationId\", \"status\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) {\n return incoming;\n }\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n developerOrgMembers: {\n keyArgs: [\"organizationId\", \"role\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n developerOrgInvitations: {\n keyArgs: [\"organizationId\", \"status\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n webhooks: {\n keyArgs: [\"applicationId\", \"status\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n webhookDeliveries: {\n keyArgs: [\"webhookId\", \"status\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n auditLogs: {\n keyArgs: [\"resource\", \"resourceId\", \"actorId\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n return {\n ...incoming,\n edges: [...(existing?.edges || []), ...incoming.edges],\n };\n },\n },\n applicationVersions: {\n // TODO(phase1-backend): Switch to ApplicationVersionConnection cursor pagination\n // once backend ships connection type. See stability tracker.\n keyArgs: [\"applicationId\"],\n merge(existing, incoming, { args }) {\n if (!args?.offset || args.offset === 0) return incoming;\n if (Array.isArray(incoming) && Array.isArray(existing)) {\n return [...existing, ...incoming];\n }\n return incoming;\n },\n },\n },\n },\n WebhookDelivery: {\n keyFields: [\"id\"],\n },\n ApplicationVersion: {\n keyFields: [\"id\"],\n },\n DeveloperOrganization: {\n keyFields: [\"id\"],\n },\n Application: {\n keyFields: [\"id\"],\n },\n DeveloperOrgMember: {\n keyFields: [\"id\"],\n },\n DeveloperOrgInvitation: {\n keyFields: [\"id\"],\n },\n Webhook: {\n keyFields: [\"id\"],\n },\n ApiKey: {\n keyFields: [\"id\"],\n },\n OAuthClient: {\n keyFields: [\"id\"],\n },\n },\n }),\n defaultOptions: {\n watchQuery: {\n fetchPolicy: \"cache-and-network\",\n },\n query: {\n fetchPolicy: \"cache-first\",\n },\n },\n});\n\n/**\n * Error handling utilities\n */\nexport interface GraphQLErrorInfo {\n message: string;\n code?: string;\n path?: readonly (string | number)[];\n}\n\n// Type for Apollo errors (can be CombinedGraphQLErrors, ServerError, or Error)\nexport type ApolloErrorLike = Error & {\n graphQLErrors?: readonly {\n message: string;\n extensions?: { code?: string };\n path?: readonly (string | number)[];\n }[];\n networkError?: Error | null;\n};\n\nexport function parseGraphQLError(error: ApolloErrorLike): GraphQLErrorInfo[] {\n if (error instanceof CombinedGraphQLErrors) {\n return error.errors.map((err) => ({\n message: err.message,\n code: err.extensions?.code as string | undefined,\n path: err.path,\n }));\n }\n\n if (error.graphQLErrors && error.graphQLErrors.length > 0) {\n return error.graphQLErrors.map((err) => ({\n message: err.message,\n code: err.extensions?.code as string | undefined,\n path: err.path,\n }));\n }\n\n if (error instanceof ServerError || error.networkError) {\n return [\n {\n message: \"Network error. Please check your connection and try again.\",\n code: \"NETWORK_ERROR\",\n },\n ];\n }\n\n return [\n {\n message: error.message || \"An unexpected error occurred\",\n code: \"UNKNOWN_ERROR\",\n },\n ];\n}\n\n/**\n * Get a user-friendly error message from an Apollo error\n */\nexport function getErrorMessage(error: ApolloErrorLike): string {\n const errors = parseGraphQLError(error);\n return errors.map((e) => e.message).join(\", \");\n}\n\n/**\n * Check if an error is an authentication error\n */\nexport function isAuthError(error: ApolloErrorLike): boolean {\n const errors = parseGraphQLError(error);\n return errors.some(\n (e) => e.code === \"UNAUTHENTICATED\" || e.code === \"FORBIDDEN\",\n );\n}\n\n/**\n * Check if an error is a validation error\n */\nexport function isValidationError(error: ApolloErrorLike): boolean {\n const errors = parseGraphQLError(error);\n return errors.some(\n (e) => e.code === \"BAD_USER_INPUT\" || e.code === \"VALIDATION_ERROR\",\n );\n}\n\n/**\n * Clear Apollo cache (useful for logout)\n */\nexport async function clearApolloCache(): Promise<void> {\n await apolloClient.clearStore();\n}\n\n/**\n * Reset Apollo store and refetch queries\n */\nexport async function resetApolloStore(): Promise<void> {\n await apolloClient.resetStore();\n}\n"],"mappings":";;;;;;;;AAuBA,SAAS,EACP,IAGK,EAAE,EACE;AACT,QAAO,EAAO,MAAM,MAAU;EAC5B,IAAM,IACJ,OAAO,EAAM,YAAY,QAAS,WAAW,EAAM,WAAW,OAAO,IACjE,IACJ,OAAO,EAAM,YAAY,aAAc,WACnC,EAAM,WAAW,YACjB,IACA,KAAW,EAAM,WAAW,IAAI,aAAa;AAEnD,SACE,MAAS,qBACT,MAAc,mBACd,EAAQ,SAAS,gBAAgB,IACjC,EAAQ,SAAS,gBAAgB,IACjC,EAAQ,SAAS,kBAAkB,IACnC,EAAQ,SAAS,eAAe,IAChC,EAAQ,SAAS,kBAAkB,IACnC,EAAQ,SAAS,0BAA0B,IAC3C,EAAQ,SAAS,4BAA4B;GAE/C;;AAqBJ,IAAM,IACkC,IAIlC,IAGH,IAAgB,uBAAuB,iCAEpC,IAGH,IAAgB,kBAAkB,iCAE/B,IAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC,EAII,IAEJ,gBAEI,IAGJ,QAGI,IAAoB,EAAe,EACvC,KAAK,GACN,CAAC,EAMI,IAAW,GACd,EAAE,uBACI,IAIE,EAAqB,IAAI,EAAc,GAHrC,IAPQ,EAAe,EAClC,KAAK,GACN,CAAC,EAWA,EACD,EAGK,IAAW,GAAY,GAAG,EAAE,iBAAc;CAE9C,IAAM,IACJ,OAAO,SAAW,MAAc,aAAa,QAAQ,aAAa,GAAG,MAGjE,IAAsC,EAC1C,GAAG,GACJ;AAQD,KANI,MACF,EAAY,gBAAmB,UAAU,MAKvC,GAAe;EAEjB,IAAM,IACH,OAAO,SAAW,OAAe,aAAa,QAAQ,eAAe,IACtE,GACI,IACH,OAAO,SAAW,OACjB,aAAa,QAAQ,iBAAiB,IACxC;AAGF,EADA,EAAY,gBAAgB,GAC5B,EAAY,kBAAkB;;AAGhC,QAAO,EAAE,SAAS,GAAa;EAC/B,EAwBW,IAAe,IAAI,EAAa;CAC3C,MAAM,EAAK;EAvBK,GAAS,EAAE,eAAY;AAClC,UAKL;QAAI,EAFF,YAAY,KAAS,MAAM,QAAQ,EAAM,OAAO,GAAG,EAAM,SAAS,EAAE,CAE/B,EAAE;AACvC,QAAiB;AACjB;;AAGF,IAAI,aAAiB,SAAS,EAAmB,EAAM,IACrD,GAAiB;;IAEnB;EASuB;EAAU;EAAS,CAAC;CAC3C,OAAO,IAAI,EAAc,EACvB,cAAc;EACZ,OAAO,EACL,QAAQ;GAEN,wBAAwB;IACtB,SAAS;KAAC;KAAQ;KAAc;KAAe;KAAc;IAC7D,MAAM,GAAU,GAAU,EAAE,WAAQ;AAIlC,YAHI,CAAC,GAAM,UAAU,EAAK,WAAW,IAC5B,IAEF;MACL,GAAG;MACH,OAAO,CAAC,GAAI,GAAU,SAAS,EAAE,EAAG,GAAG,EAAS,MAAM;MACvD;;IAEJ;GACD,cAAc;IACZ,SAAS;KAAC;KAAU;KAAc;KAAc;IAChD,MAAM,GAAU,GAAU,EAAE,WAAQ;AAIlC,YAHI,CAAC,GAAM,UAAU,EAAK,WAAW,IAC5B,IAEF;MACL,GAAG;MACH,OAAO,CAAC,GAAI,GAAU,SAAS,EAAE,EAAG,GAAG,EAAS,MAAM;MACvD;;IAEJ;GACD,mBAAmB;IACjB,SAAS;KACP;KACA;KACA;KACA;KACA;KACD;IACD,MAAM,GAAU,GAAU,EAAE,WAAQ;AAIlC,YAHI,CAAC,GAAM,UAAU,EAAK,WAAW,IAC5B,IAEF;MACL,GAAG;MACH,OAAO,CAAC,GAAI,GAAU,SAAS,EAAE,EAAG,GAAG,EAAS,MAAM;MACvD;;IAEJ;GACD,qBAAqB;IACnB,SAAS,CAAC,kBAAkB,SAAS;IACrC,MAAM,GAAU,GAAU,EAAE,WAAQ;AAIlC,YAHI,CAAC,GAAM,UAAU,EAAK,WAAW,IAC5B,IAEF;MACL,GAAG;MACH,OAAO,CAAC,GAAI,GAAU,SAAS,EAAE,EAAG,GAAG,EAAS,MAAM;MACvD;;IAEJ;GACD,qBAAqB;IACnB,SAAS,CAAC,kBAAkB,OAAO;IACnC,MAAM,GAAU,GAAU,EAAE,WAAQ;AAElC,YADI,CAAC,GAAM,UAAU,EAAK,WAAW,IAAU,IACxC;MACL,GAAG;MACH,OAAO,CAAC,GAAI,GAAU,SAAS,EAAE,EAAG,GAAG,EAAS,MAAM;MACvD;;IAEJ;GACD,yBAAyB;IACvB,SAAS,CAAC,kBAAkB,SAAS;IACrC,MAAM,GAAU,GAAU,EAAE,WAAQ;AAElC,YADI,CAAC,GAAM,UAAU,EAAK,WAAW,IAAU,IACxC;MACL,GAAG;MACH,OAAO,CAAC,GAAI,GAAU,SAAS,EAAE,EAAG,GAAG,EAAS,MAAM;MACvD;;IAEJ;GACD,UAAU;IACR,SAAS,CAAC,iBAAiB,SAAS;IACpC,MAAM,GAAU,GAAU,EAAE,WAAQ;AAElC,YADI,CAAC,GAAM,UAAU,EAAK,WAAW,IAAU,IACxC;MACL,GAAG;MACH,OAAO,CAAC,GAAI,GAAU,SAAS,EAAE,EAAG,GAAG,EAAS,MAAM;MACvD;;IAEJ;GACD,mBAAmB;IACjB,SAAS,CAAC,aAAa,SAAS;IAChC,MAAM,GAAU,GAAU,EAAE,WAAQ;AAElC,YADI,CAAC,GAAM,UAAU,EAAK,WAAW,IAAU,IACxC;MACL,GAAG;MACH,OAAO,CAAC,GAAI,GAAU,SAAS,EAAE,EAAG,GAAG,EAAS,MAAM;MACvD;;IAEJ;GACD,WAAW;IACT,SAAS;KAAC;KAAY;KAAc;KAAU;IAC9C,MAAM,GAAU,GAAU,EAAE,WAAQ;AAElC,YADI,CAAC,GAAM,UAAU,EAAK,WAAW,IAAU,IACxC;MACL,GAAG;MACH,OAAO,CAAC,GAAI,GAAU,SAAS,EAAE,EAAG,GAAG,EAAS,MAAM;MACvD;;IAEJ;GACD,qBAAqB;IAGnB,SAAS,CAAC,gBAAgB;IAC1B,MAAM,GAAU,GAAU,EAAE,WAAQ;AAKlC,YAJI,CAAC,GAAM,UAAU,EAAK,WAAW,IAAU,IAC3C,MAAM,QAAQ,EAAS,IAAI,MAAM,QAAQ,EAAS,GAC7C,CAAC,GAAG,GAAU,GAAG,EAAS,GAE5B;;IAEV;GACF,EACF;EACD,iBAAiB,EACf,WAAW,CAAC,KAAK,EAClB;EACD,oBAAoB,EAClB,WAAW,CAAC,KAAK,EAClB;EACD,uBAAuB,EACrB,WAAW,CAAC,KAAK,EAClB;EACD,aAAa,EACX,WAAW,CAAC,KAAK,EAClB;EACD,oBAAoB,EAClB,WAAW,CAAC,KAAK,EAClB;EACD,wBAAwB,EACtB,WAAW,CAAC,KAAK,EAClB;EACD,SAAS,EACP,WAAW,CAAC,KAAK,EAClB;EACD,QAAQ,EACN,WAAW,CAAC,KAAK,EAClB;EACD,aAAa,EACX,WAAW,CAAC,KAAK,EAClB;EACF,EACF,CAAC;CACF,gBAAgB;EACd,YAAY,EACV,aAAa,qBACd;EACD,OAAO,EACL,aAAa,eACd;EACF;CACF,CAAC;AAqBF,SAAgB,EAAkB,GAA4C;AA0B5E,QAzBI,aAAiB,IACZ,EAAM,OAAO,KAAK,OAAS;EAChC,SAAS,EAAI;EACb,MAAM,EAAI,YAAY;EACtB,MAAM,EAAI;EACX,EAAE,GAGD,EAAM,iBAAiB,EAAM,cAAc,SAAS,IAC/C,EAAM,cAAc,KAAK,OAAS;EACvC,SAAS,EAAI;EACb,MAAM,EAAI,YAAY;EACtB,MAAM,EAAI;EACX,EAAE,GAGD,aAAiB,KAAe,EAAM,eACjC,CACL;EACE,SAAS;EACT,MAAM;EACP,CACF,GAGI,CACL;EACE,SAAS,EAAM,WAAW;EAC1B,MAAM;EACP,CACF;;AAMH,SAAgB,EAAgB,GAAgC;AAE9D,QADe,EAAkB,EAAM,CACzB,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;;AAMhD,SAAgB,EAAY,GAAiC;AAE3D,QADe,EAAkB,EAAM,CACzB,MACX,MAAM,EAAE,SAAS,qBAAqB,EAAE,SAAS,YACnD;;AAMH,SAAgB,EAAkB,GAAiC;AAEjE,QADe,EAAkB,EAAM,CACzB,MACX,MAAM,EAAE,SAAS,oBAAoB,EAAE,SAAS,mBAClD;;AAMH,eAAsB,IAAkC;AACtD,OAAM,EAAa,YAAY;;AAMjC,eAAsB,IAAkC;AACtD,OAAM,EAAa,YAAY"}
@@ -39,7 +39,7 @@ var g = (e) => {
39
39
  let { navigate: v, basePath: he } = e(), { t: y } = se(), b = (e, t) => {
40
40
  let n = y(e);
41
41
  return n === e ? t : n;
42
- }, [x, ge] = l(""), [S, C] = l("all"), [w, T] = l(null), [E, D] = l(null), [O, k] = l(null), [A, j] = l(/* @__PURE__ */ new Set()), [M, N] = l(null), [P, F] = l(!1), { data: I, loading: L, error: R, refetch: z } = te({ fetchPolicy: "cache-and-network" }), [B, { loading: V }] = ne(), [_e, { loading: H }] = re(), U = c(() => I?.myApiKeys?.map(me) ?? [], [I?.myApiKeys]), W = (e) => {
42
+ }, [x, ge] = l(""), [S, C] = l("all"), [w, T] = l(null), [E, D] = l(null), [O, k] = l(null), [A, j] = l(/* @__PURE__ */ new Set()), [M, N] = l(null), [P, F] = l(!1), { data: I, loading: L, error: R, refetch: z } = te({ fetchPolicy: "cache-and-network" }), [B, { loading: V }] = ne(), [_e, { loading: H }] = re(), U = c(() => I?.myApiKeys?.apiKeys.map(me) ?? [], [I?.myApiKeys?.apiKeys]), W = (e) => {
43
43
  v?.(`${he}${e}`);
44
44
  }, ve = async () => {
45
45
  if (w) try {
@@ -1 +1 @@
1
- {"version":3,"file":"ApiKeysPage.js","names":[],"sources":["../../../src/devportal/pages/ApiKeysPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useMemo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n Plus,\n Search,\n Key,\n Shield,\n Clock,\n AlertTriangle,\n Loader2,\n AlertCircle,\n RotateCw,\n Ban,\n} from \"lucide-react\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { DataTable } from \"../components/DataTable\";\nimport { ActiveIndicator } from \"../components/StatusBadge\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\nimport { NewSecretAlert } from \"../components/SecretDisplay\";\nimport { SelectionCheckbox } from \"../components/SelectionCheckbox\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport type { AuthApiKey } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport {\n useMyApiKeysQuery,\n useRevokeApiKeyAuthMutation,\n useRotateApiKeyAuthMutation,\n getErrorMessage,\n} from \"../hooks\";\n\n/**\n * Map GraphQL APIKey to local AuthApiKey type\n */\nfunction mapApiKey(key: {\n id: string;\n name: string;\n prefix: string;\n scopes: string[];\n appId: string;\n appName?: string | null;\n createdAt: string;\n expiresAt?: string | null;\n lastUsedAt?: string | null;\n}): AuthApiKey {\n return {\n id: key.id,\n name: key.name,\n prefix: key.prefix,\n scopes: key.scopes,\n appId: key.appId,\n appName: key.appName ?? undefined,\n createdAt: key.createdAt,\n expiresAt: key.expiresAt ?? undefined,\n lastUsedAt: key.lastUsedAt ?? undefined,\n };\n}\n\nconst isExpiringSoon = (expiresAt?: string): boolean => {\n if (!expiresAt) return false;\n const daysUntilExpiry = Math.ceil(\n (new Date(expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24),\n );\n return daysUntilExpiry <= 30 && daysUntilExpiry > 0;\n};\n\nconst isExpired = (expiresAt?: string): boolean => {\n if (!expiresAt) return false;\n return new Date(expiresAt).getTime() < Date.now();\n};\n\n/**\n * API Keys listing page\n *\n * Displays all API keys for the current user from auth-svc.\n * Previously showed devportal-svc ApiKeys per application;\n * now shows all user-owned API keys centrally.\n * Supports revoking and rotating API keys.\n */\nexport const ApiKeysPage: FC = () => {\n const { navigate, basePath } = useDevportal();\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 [searchQuery, setSearchQuery] = useState(\"\");\n const [statusFilter, setStatusFilter] = useState<\n \"all\" | \"active\" | \"expired\"\n >(\"all\");\n\n // Revoke / Rotate state\n const [revokeTarget, setRevokeTarget] = useState<AuthApiKey | null>(null);\n const [rotateTarget, setRotateTarget] = useState<AuthApiKey | null>(null);\n const [newSecret, setNewSecret] = useState<string | null>(null);\n const [selectedKeyIds, setSelectedKeyIds] = useState<Set<string>>(new Set());\n const [bulkFeedback, setBulkFeedback] = useState<{\n type: \"success\" | \"error\";\n message: string;\n } | null>(null);\n const [showBulkRevokeDialog, setShowBulkRevokeDialog] = useState(false);\n\n // Fetch API keys from auth-svc (user-scoped, no applicationId needed)\n const { data, loading, error, refetch } = useMyApiKeysQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n // Mutations\n const [revokeApiKey, { loading: revokeLoading }] =\n useRevokeApiKeyAuthMutation();\n const [rotateApiKey, { loading: rotateLoading }] =\n useRotateApiKeyAuthMutation();\n\n const apiKeys: AuthApiKey[] = useMemo(\n () => data?.myApiKeys?.map(mapApiKey) ?? [],\n [data?.myApiKeys],\n );\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n // Revoke handler\n const handleRevoke = async () => {\n if (!revokeTarget) return;\n try {\n await revokeApiKey({\n variables: { keyId: revokeTarget.id },\n });\n setRevokeTarget(null);\n await refetch();\n } catch {\n // Error is surfaced by Apollo's error state on the mutation;\n // the ConfirmDialog stays open so the user can retry or cancel.\n }\n };\n\n // Rotate handler\n const handleRotate = async () => {\n if (!rotateTarget) return;\n try {\n const result = await rotateApiKey({\n variables: { keyId: rotateTarget.id },\n });\n const secret = result.data?.rotateAPIKey.apiSecret;\n if (secret) {\n setNewSecret(secret);\n }\n setRotateTarget(null);\n await refetch();\n } catch {\n // Error is surfaced by Apollo's error state on the mutation;\n // the ConfirmDialog stays open so the user can retry or cancel.\n }\n };\n\n // Filter keys\n const filteredKeys = apiKeys.filter((key: AuthApiKey) => {\n const matchesSearch =\n !searchQuery ||\n key.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n key.prefix.toLowerCase().includes(searchQuery.toLowerCase()) ||\n (key.appName?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false);\n const matchesStatus =\n statusFilter === \"all\" ||\n (statusFilter === \"active\" && !isExpired(key.expiresAt)) ||\n (statusFilter === \"expired\" && isExpired(key.expiresAt));\n return matchesSearch && matchesStatus;\n });\n\n const selectedKeys = useMemo(\n () => apiKeys.filter((key) => selectedKeyIds.has(key.id)),\n [apiKeys, selectedKeyIds],\n );\n\n const selectedActiveKeys = useMemo(\n () => selectedKeys.filter((key) => !isExpired(key.expiresAt)),\n [selectedKeys],\n );\n\n const allVisibleSelectableKeys = filteredKeys.filter(\n (key) => !isExpired(key.expiresAt),\n );\n const allVisibleSelected =\n allVisibleSelectableKeys.length > 0 &&\n allVisibleSelectableKeys.every((key) => selectedKeyIds.has(key.id));\n const partiallySelected =\n allVisibleSelectableKeys.some((key) => selectedKeyIds.has(key.id)) &&\n !allVisibleSelected;\n\n const toggleKeySelection = (keyId: string, checked: boolean) => {\n setSelectedKeyIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(keyId);\n } else {\n next.delete(keyId);\n }\n return next;\n });\n };\n\n const toggleAllVisibleKeys = (checked: boolean) => {\n setSelectedKeyIds((prev) => {\n const next = new Set(prev);\n allVisibleSelectableKeys.forEach((key) => {\n if (checked) {\n next.add(key.id);\n } else {\n next.delete(key.id);\n }\n });\n return next;\n });\n };\n\n const clearSelection = () => setSelectedKeyIds(new Set());\n\n // Loading state\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\n )}\n >\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n <span className=\"ml-2 text-muted-foreground\">\n {tr(\"devportal.loadingApiKeys\", \"Loading API keys...\")}\n </span>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\n )}\n >\n <div className=\"rounded-lg border border-destructive/50 bg-destructive/10 p-6 text-center\">\n <AlertCircle className=\"mx-auto h-8 w-8 text-destructive\" />\n <h3 className=\"mt-2 font-medium text-destructive\">\n {tr(\"devportal.failedToLoadApiKeys\", \"Failed to load API keys\")}\n </h3>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {getErrorMessage(error)}\n </p>\n <button\n onClick={() => refetch()}\n className=\"mt-4 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90\"\n >\n {tr(\"devportal.tryAgain\", \"Try Again\")}\n </button>\n </div>\n </PageLayout>\n );\n }\n\n const columns = [\n {\n key: \"select\",\n header: (\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisibleKeys}\n ariaLabel=\"Select all visible API keys\"\n />\n ),\n className: \"w-12\",\n render: (key: AuthApiKey) => (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selectedKeyIds.has(key.id)}\n disabled={isExpired(key.expiresAt)}\n onChange={(checked) => toggleKeySelection(key.id, checked)}\n ariaLabel={`Select API key ${key.name}`}\n />\n </div>\n ),\n },\n {\n key: \"name\",\n header: \"API Key\",\n render: (key: AuthApiKey) => (\n <div className=\"flex items-center gap-3\">\n <div\n className={cn(\n \"flex h-10 w-10 items-center justify-center rounded-lg\",\n isExpired(key.expiresAt)\n ? \"bg-status-error-bg-subtle text-status-error-text\"\n : \"bg-primary/10 text-primary\",\n )}\n >\n <Key size={20} />\n </div>\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium\">{key.name}</p>\n {isExpired(key.expiresAt) && (\n <span className=\"rounded bg-status-error-bg-subtle px-1.5 py-0.5 text-xs text-status-error-text\">\n Expired\n </span>\n )}\n {isExpiringSoon(key.expiresAt) && !isExpired(key.expiresAt) && (\n <span className=\"rounded bg-status-warning-bg-subtle px-1.5 py-0.5 text-xs text-status-warning-text\">\n Expiring Soon\n </span>\n )}\n </div>\n {key.appName && (\n <p className=\"text-xs text-muted-foreground\">\n App: {key.appName}\n </p>\n )}\n </div>\n </div>\n ),\n },\n {\n key: \"prefix\",\n header: \"Key Prefix\",\n render: (key: AuthApiKey) => (\n <code className=\"text-sm text-muted-foreground\">{key.prefix}...</code>\n ),\n },\n {\n key: \"scopes\",\n header: \"Scopes\",\n render: (key: AuthApiKey) => (\n <div className=\"flex flex-wrap gap-1\">\n {key.scopes.slice(0, 3).map((scope) => (\n <span\n key={scope}\n className=\"inline-flex rounded bg-muted px-1.5 py-0.5 text-xs\"\n >\n {scope}\n </span>\n ))}\n {key.scopes.length > 3 && (\n <span className=\"text-xs text-muted-foreground\">\n +{key.scopes.length - 3}\n </span>\n )}\n </div>\n ),\n },\n {\n key: \"lastUsed\",\n header: \"Last Used\",\n render: (key: AuthApiKey) => (\n <span className=\"text-sm text-muted-foreground\">\n {key.lastUsedAt\n ? new Date(key.lastUsedAt).toLocaleDateString()\n : \"Never\"}\n </span>\n ),\n },\n {\n key: \"expires\",\n header: \"Expires\",\n render: (key: AuthApiKey) => (\n <span\n className={cn(\n \"text-sm\",\n isExpired(key.expiresAt)\n ? \"text-status-error-text\"\n : isExpiringSoon(key.expiresAt)\n ? \"text-status-warning-text\"\n : \"text-muted-foreground\",\n )}\n >\n {key.expiresAt\n ? new Date(key.expiresAt).toLocaleDateString()\n : \"Never\"}\n </span>\n ),\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (key: AuthApiKey) => (\n <div className=\"flex items-center gap-2\">\n <ActiveIndicator active={!isExpired(key.expiresAt)} />\n <span className=\"text-sm\">\n {isExpired(key.expiresAt) ? \"Expired\" : \"Active\"}\n </span>\n </div>\n ),\n },\n {\n key: \"actions\",\n header: \"Actions\",\n className: \"w-28\",\n render: (key: AuthApiKey) => {\n if (isExpired(key.expiresAt)) {\n return (\n <span className=\"text-xs text-muted-foreground\">No actions</span>\n );\n }\n\n return (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n title=\"Rotate secret\"\n disabled={rotateLoading}\n onClick={(e) => {\n e.stopPropagation();\n setRotateTarget(key);\n }}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n \"text-status-info-text hover:bg-status-info-bg-subtle\",\n \"disabled:opacity-50 disabled:cursor-not-allowed\",\n )}\n >\n <RotateCw size={14} />\n <span className=\"sr-only sm:not-sr-only\">Rotate</span>\n </button>\n <button\n type=\"button\"\n title=\"Revoke key\"\n disabled={revokeLoading}\n onClick={(e) => {\n e.stopPropagation();\n setRevokeTarget(key);\n }}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n \"text-status-error-text hover:bg-status-error-bg-subtle\",\n \"disabled:opacity-50 disabled:cursor-not-allowed\",\n )}\n >\n <Ban size={14} />\n <span className=\"sr-only sm:not-sr-only\">Revoke</span>\n </button>\n </div>\n );\n },\n },\n ];\n\n // Stats\n const stats = {\n total: apiKeys.length,\n active: apiKeys.filter((k) => !isExpired(k.expiresAt)).length,\n expiringSoon: apiKeys.filter((k) => isExpiringSoon(k.expiresAt)).length,\n expired: apiKeys.filter((k) => isExpired(k.expiresAt)).length,\n };\n\n const handleBulkRevoke = async () => {\n if (selectedActiveKeys.length === 0) return;\n\n const results = await Promise.allSettled(\n selectedActiveKeys.map((key) =>\n revokeApiKey({ variables: { keyId: key.id } }),\n ),\n );\n\n const succeeded = results.filter(\n (result) => result.status === \"fulfilled\",\n ).length;\n const failed = results.length - succeeded;\n\n setBulkFeedback({\n type: failed === 0 ? \"success\" : \"error\",\n message: `Revoked ${succeeded} API key${succeeded === 1 ? \"\" : \"s\"}${\n failed > 0 ? ` • ${failed} failed` : \"\"\n }`,\n });\n setShowBulkRevokeDialog(false);\n clearSelection();\n await refetch();\n };\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.APP_READ}>\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\n )}\n actions={\n <button\n onClick={() => handleNavigate(\"/api-keys/new\")}\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 <Plus size={16} />\n {tr(\"devportal.generateApiKey\", \"Generate API Key\")}\n </button>\n }\n >\n {/* New secret alert — shown after a successful rotation */}\n {newSecret && (\n <NewSecretAlert\n title=\"API Key Rotated\"\n secret={newSecret}\n description=\"The previous secret has been invalidated. Copy the new secret below — it will not be shown again.\"\n onDismiss={() => setNewSecret(null)}\n />\n )}\n\n {bulkFeedback && (\n <div\n className={cn(\n \"rounded-lg border px-4 py-3 text-sm\",\n bulkFeedback.type === \"success\"\n ? \"border-border-subtle bg-status-success-bg-subtle text-status-success-text\"\n : \"border-destructive/40 bg-destructive/10 text-destructive\",\n )}\n >\n <div className=\"flex items-center justify-between gap-3\">\n <span>{bulkFeedback.message}</span>\n <button\n onClick={() => setBulkFeedback(null)}\n className=\"text-xs font-medium underline underline-offset-2\"\n >\n Dismiss\n </button>\n </div>\n </div>\n )}\n\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-1 items-center gap-4\">\n {/* Search */}\n <div className=\"relative flex-1 max-w-md\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n />\n <input\n type=\"text\"\n placeholder=\"Search by name, prefix, or app...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-md border bg-background pl-9 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n\n {/* Status Filter */}\n <select\n value={statusFilter}\n onChange={(e) =>\n setStatusFilter(e.target.value as \"all\" | \"active\" | \"expired\")\n }\n className=\"rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"all\">All Status</option>\n <option value=\"active\">Active</option>\n <option value=\"expired\">Expired</option>\n </select>\n </div>\n </div>\n\n {/* Summary Stats */}\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground\">\n <Key size={16} />\n <span className=\"text-sm\">Total Keys</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold\">{stats.total}</p>\n </div>\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"flex items-center gap-2 text-status-success-text\">\n <Shield size={16} />\n <span className=\"text-sm\">Active</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold\">{stats.active}</p>\n </div>\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"flex items-center gap-2 text-status-warning-text\">\n <Clock size={16} />\n <span className=\"text-sm\">Expiring Soon</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold\">{stats.expiringSoon}</p>\n </div>\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"flex items-center gap-2 text-status-error-text\">\n <AlertTriangle size={16} />\n <span className=\"text-sm\">Expired</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold\">{stats.expired}</p>\n </div>\n </div>\n\n {/* Results count */}\n <p className=\"text-sm text-muted-foreground\">\n {filteredKeys.length} API key{filteredKeys.length !== 1 ? \"s\" : \"\"}{\" \"}\n found\n </p>\n\n {filteredKeys.length > 0 && (\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex items-center gap-2\">\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisibleKeys}\n ariaLabel=\"Select all visible API keys\"\n />\n <span className=\"text-sm font-medium\">\n Select all active keys\n </span>\n </div>\n {selectedKeyIds.size > 0 && (\n <button\n onClick={clearSelection}\n className=\"text-sm text-muted-foreground hover:text-foreground\"\n >\n Clear selection\n </button>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-full bg-muted px-3 py-1 text-sm font-medium text-muted-foreground\">\n {selectedKeyIds.size} selected\n </span>\n <button\n onClick={() => setShowBulkRevokeDialog(true)}\n disabled={selectedActiveKeys.length === 0}\n className=\"inline-flex items-center gap-2 rounded-md border border-status-error-bg px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <Ban size={16} />\n Revoke Selected\n {selectedActiveKeys.length > 0\n ? ` (${selectedActiveKeys.length})`\n : \"\"}\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* Keys Table */}\n {filteredKeys.length > 0 ? (\n <DataTable data={filteredKeys} columns={columns} keyField=\"id\" />\n ) : (\n <EmptyState\n icon={<Key size={32} />}\n title={tr(\"devportal.noApiKeysFound\", \"No API keys found\")}\n description={\n searchQuery || statusFilter !== \"all\"\n ? tr(\n \"devportal.tryAdjustingFilters\",\n \"Try adjusting your filters\",\n )\n : tr(\n \"devportal.generateFirstApiKey\",\n \"Generate your first API key to start making API calls\",\n )\n }\n action={\n !searchQuery && statusFilter === \"all\" ? (\n <button\n onClick={() => handleNavigate(\"/api-keys/new\")}\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\"\n >\n <Plus size={16} />\n {tr(\"devportal.generateApiKey\", \"Generate API Key\")}\n </button>\n ) : undefined\n }\n />\n )}\n\n {/* Revoke confirmation dialog */}\n <ConfirmDialog\n open={revokeTarget !== null}\n onOpenChange={(open) => {\n if (!open) setRevokeTarget(null);\n }}\n onConfirm={handleRevoke}\n title=\"Revoke API Key\"\n description={`Are you sure you want to revoke \"${revokeTarget?.name ?? \"\"}\"? This action cannot be undone. Any application or service using this key will immediately lose access.`}\n confirmLabel=\"Revoke Key\"\n cancelLabel=\"Cancel\"\n variant=\"danger\"\n loading={revokeLoading}\n />\n\n {/* Rotate confirmation dialog */}\n <ConfirmDialog\n open={rotateTarget !== null}\n onOpenChange={(open) => {\n if (!open) setRotateTarget(null);\n }}\n onConfirm={handleRotate}\n title=\"Rotate API Key Secret\"\n description={`This will generate a new secret for \"${rotateTarget?.name ?? \"\"}\" and immediately invalidate the current one. Make sure to update all services using this key before the old secret stops working.`}\n confirmLabel=\"Rotate Secret\"\n cancelLabel=\"Cancel\"\n variant=\"warning\"\n loading={rotateLoading}\n />\n\n <ConfirmDialog\n open={showBulkRevokeDialog}\n onOpenChange={setShowBulkRevokeDialog}\n onConfirm={handleBulkRevoke}\n title=\"Revoke selected API keys\"\n description={`Revoke ${selectedActiveKeys.length} selected API key${selectedActiveKeys.length === 1 ? \"\" : \"s\"}? Any service using these keys will immediately lose access.`}\n confirmLabel=\"Revoke Keys\"\n cancelLabel=\"Cancel\"\n variant=\"danger\"\n loading={revokeLoading}\n />\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"ApiKeysPage.js","names":[],"sources":["../../../src/devportal/pages/ApiKeysPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useMemo, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n Plus,\n Search,\n Key,\n Shield,\n Clock,\n AlertTriangle,\n Loader2,\n AlertCircle,\n RotateCw,\n Ban,\n} from \"lucide-react\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { DataTable } from \"../components/DataTable\";\nimport { ActiveIndicator } from \"../components/StatusBadge\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\nimport { NewSecretAlert } from \"../components/SecretDisplay\";\nimport { SelectionCheckbox } from \"../components/SelectionCheckbox\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\nimport { DEVPORTAL_PERMISSIONS } from \"../constants/permissions\";\nimport type { AuthApiKey } from \"../types\";\nimport { cn } from \"../utils/cn\";\nimport {\n useMyApiKeysQuery,\n useRevokeApiKeyAuthMutation,\n useRotateApiKeyAuthMutation,\n getErrorMessage,\n} from \"../hooks\";\n\n/**\n * Map GraphQL APIKey to local AuthApiKey type\n */\nfunction mapApiKey(key: {\n id: string;\n name: string;\n prefix: string;\n scopes: string[];\n appId: string;\n appName?: string | null;\n createdAt: string;\n expiresAt?: string | null;\n lastUsedAt?: string | null;\n}): AuthApiKey {\n return {\n id: key.id,\n name: key.name,\n prefix: key.prefix,\n scopes: key.scopes,\n appId: key.appId,\n appName: key.appName ?? undefined,\n createdAt: key.createdAt,\n expiresAt: key.expiresAt ?? undefined,\n lastUsedAt: key.lastUsedAt ?? undefined,\n };\n}\n\nconst isExpiringSoon = (expiresAt?: string): boolean => {\n if (!expiresAt) return false;\n const daysUntilExpiry = Math.ceil(\n (new Date(expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24),\n );\n return daysUntilExpiry <= 30 && daysUntilExpiry > 0;\n};\n\nconst isExpired = (expiresAt?: string): boolean => {\n if (!expiresAt) return false;\n return new Date(expiresAt).getTime() < Date.now();\n};\n\n/**\n * API Keys listing page\n *\n * Displays all API keys for the current user from auth-svc.\n * Previously showed devportal-svc ApiKeys per application;\n * now shows all user-owned API keys centrally.\n * Supports revoking and rotating API keys.\n */\nexport const ApiKeysPage: FC = () => {\n const { navigate, basePath } = useDevportal();\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 [searchQuery, setSearchQuery] = useState(\"\");\n const [statusFilter, setStatusFilter] = useState<\n \"all\" | \"active\" | \"expired\"\n >(\"all\");\n\n // Revoke / Rotate state\n const [revokeTarget, setRevokeTarget] = useState<AuthApiKey | null>(null);\n const [rotateTarget, setRotateTarget] = useState<AuthApiKey | null>(null);\n const [newSecret, setNewSecret] = useState<string | null>(null);\n const [selectedKeyIds, setSelectedKeyIds] = useState<Set<string>>(new Set());\n const [bulkFeedback, setBulkFeedback] = useState<{\n type: \"success\" | \"error\";\n message: string;\n } | null>(null);\n const [showBulkRevokeDialog, setShowBulkRevokeDialog] = useState(false);\n\n // Fetch API keys from auth-svc (user-scoped, no applicationId needed)\n const { data, loading, error, refetch } = useMyApiKeysQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n // Mutations\n const [revokeApiKey, { loading: revokeLoading }] =\n useRevokeApiKeyAuthMutation();\n const [rotateApiKey, { loading: rotateLoading }] =\n useRotateApiKeyAuthMutation();\n\n const apiKeys: AuthApiKey[] = useMemo(\n () => data?.myApiKeys?.apiKeys.map(mapApiKey) ?? [],\n [data?.myApiKeys?.apiKeys],\n );\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n // Revoke handler\n const handleRevoke = async () => {\n if (!revokeTarget) return;\n try {\n await revokeApiKey({\n variables: { keyId: revokeTarget.id },\n });\n setRevokeTarget(null);\n await refetch();\n } catch {\n // Error is surfaced by Apollo's error state on the mutation;\n // the ConfirmDialog stays open so the user can retry or cancel.\n }\n };\n\n // Rotate handler\n const handleRotate = async () => {\n if (!rotateTarget) return;\n try {\n const result = await rotateApiKey({\n variables: { keyId: rotateTarget.id },\n });\n const secret = result.data?.rotateAPIKey.apiSecret;\n if (secret) {\n setNewSecret(secret);\n }\n setRotateTarget(null);\n await refetch();\n } catch {\n // Error is surfaced by Apollo's error state on the mutation;\n // the ConfirmDialog stays open so the user can retry or cancel.\n }\n };\n\n // Filter keys\n const filteredKeys = apiKeys.filter((key: AuthApiKey) => {\n const matchesSearch =\n !searchQuery ||\n key.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n key.prefix.toLowerCase().includes(searchQuery.toLowerCase()) ||\n (key.appName?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false);\n const matchesStatus =\n statusFilter === \"all\" ||\n (statusFilter === \"active\" && !isExpired(key.expiresAt)) ||\n (statusFilter === \"expired\" && isExpired(key.expiresAt));\n return matchesSearch && matchesStatus;\n });\n\n const selectedKeys = useMemo(\n () => apiKeys.filter((key) => selectedKeyIds.has(key.id)),\n [apiKeys, selectedKeyIds],\n );\n\n const selectedActiveKeys = useMemo(\n () => selectedKeys.filter((key) => !isExpired(key.expiresAt)),\n [selectedKeys],\n );\n\n const allVisibleSelectableKeys = filteredKeys.filter(\n (key) => !isExpired(key.expiresAt),\n );\n const allVisibleSelected =\n allVisibleSelectableKeys.length > 0 &&\n allVisibleSelectableKeys.every((key) => selectedKeyIds.has(key.id));\n const partiallySelected =\n allVisibleSelectableKeys.some((key) => selectedKeyIds.has(key.id)) &&\n !allVisibleSelected;\n\n const toggleKeySelection = (keyId: string, checked: boolean) => {\n setSelectedKeyIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(keyId);\n } else {\n next.delete(keyId);\n }\n return next;\n });\n };\n\n const toggleAllVisibleKeys = (checked: boolean) => {\n setSelectedKeyIds((prev) => {\n const next = new Set(prev);\n allVisibleSelectableKeys.forEach((key) => {\n if (checked) {\n next.add(key.id);\n } else {\n next.delete(key.id);\n }\n });\n return next;\n });\n };\n\n const clearSelection = () => setSelectedKeyIds(new Set());\n\n // Loading state\n if (loading && !data) {\n return (\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\n )}\n >\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n <span className=\"ml-2 text-muted-foreground\">\n {tr(\"devportal.loadingApiKeys\", \"Loading API keys...\")}\n </span>\n </div>\n </PageLayout>\n );\n }\n\n // Error state\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\n )}\n >\n <div className=\"rounded-lg border border-destructive/50 bg-destructive/10 p-6 text-center\">\n <AlertCircle className=\"mx-auto h-8 w-8 text-destructive\" />\n <h3 className=\"mt-2 font-medium text-destructive\">\n {tr(\"devportal.failedToLoadApiKeys\", \"Failed to load API keys\")}\n </h3>\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {getErrorMessage(error)}\n </p>\n <button\n onClick={() => refetch()}\n className=\"mt-4 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90\"\n >\n {tr(\"devportal.tryAgain\", \"Try Again\")}\n </button>\n </div>\n </PageLayout>\n );\n }\n\n const columns = [\n {\n key: \"select\",\n header: (\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisibleKeys}\n ariaLabel=\"Select all visible API keys\"\n />\n ),\n className: \"w-12\",\n render: (key: AuthApiKey) => (\n <div onClick={(event) => event.stopPropagation()}>\n <SelectionCheckbox\n checked={selectedKeyIds.has(key.id)}\n disabled={isExpired(key.expiresAt)}\n onChange={(checked) => toggleKeySelection(key.id, checked)}\n ariaLabel={`Select API key ${key.name}`}\n />\n </div>\n ),\n },\n {\n key: \"name\",\n header: \"API Key\",\n render: (key: AuthApiKey) => (\n <div className=\"flex items-center gap-3\">\n <div\n className={cn(\n \"flex h-10 w-10 items-center justify-center rounded-lg\",\n isExpired(key.expiresAt)\n ? \"bg-status-error-bg-subtle text-status-error-text\"\n : \"bg-primary/10 text-primary\",\n )}\n >\n <Key size={20} />\n </div>\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium\">{key.name}</p>\n {isExpired(key.expiresAt) && (\n <span className=\"rounded bg-status-error-bg-subtle px-1.5 py-0.5 text-xs text-status-error-text\">\n Expired\n </span>\n )}\n {isExpiringSoon(key.expiresAt) && !isExpired(key.expiresAt) && (\n <span className=\"rounded bg-status-warning-bg-subtle px-1.5 py-0.5 text-xs text-status-warning-text\">\n Expiring Soon\n </span>\n )}\n </div>\n {key.appName && (\n <p className=\"text-xs text-muted-foreground\">\n App: {key.appName}\n </p>\n )}\n </div>\n </div>\n ),\n },\n {\n key: \"prefix\",\n header: \"Key Prefix\",\n render: (key: AuthApiKey) => (\n <code className=\"text-sm text-muted-foreground\">{key.prefix}...</code>\n ),\n },\n {\n key: \"scopes\",\n header: \"Scopes\",\n render: (key: AuthApiKey) => (\n <div className=\"flex flex-wrap gap-1\">\n {key.scopes.slice(0, 3).map((scope) => (\n <span\n key={scope}\n className=\"inline-flex rounded bg-muted px-1.5 py-0.5 text-xs\"\n >\n {scope}\n </span>\n ))}\n {key.scopes.length > 3 && (\n <span className=\"text-xs text-muted-foreground\">\n +{key.scopes.length - 3}\n </span>\n )}\n </div>\n ),\n },\n {\n key: \"lastUsed\",\n header: \"Last Used\",\n render: (key: AuthApiKey) => (\n <span className=\"text-sm text-muted-foreground\">\n {key.lastUsedAt\n ? new Date(key.lastUsedAt).toLocaleDateString()\n : \"Never\"}\n </span>\n ),\n },\n {\n key: \"expires\",\n header: \"Expires\",\n render: (key: AuthApiKey) => (\n <span\n className={cn(\n \"text-sm\",\n isExpired(key.expiresAt)\n ? \"text-status-error-text\"\n : isExpiringSoon(key.expiresAt)\n ? \"text-status-warning-text\"\n : \"text-muted-foreground\",\n )}\n >\n {key.expiresAt\n ? new Date(key.expiresAt).toLocaleDateString()\n : \"Never\"}\n </span>\n ),\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (key: AuthApiKey) => (\n <div className=\"flex items-center gap-2\">\n <ActiveIndicator active={!isExpired(key.expiresAt)} />\n <span className=\"text-sm\">\n {isExpired(key.expiresAt) ? \"Expired\" : \"Active\"}\n </span>\n </div>\n ),\n },\n {\n key: \"actions\",\n header: \"Actions\",\n className: \"w-28\",\n render: (key: AuthApiKey) => {\n if (isExpired(key.expiresAt)) {\n return (\n <span className=\"text-xs text-muted-foreground\">No actions</span>\n );\n }\n\n return (\n <div className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n title=\"Rotate secret\"\n disabled={rotateLoading}\n onClick={(e) => {\n e.stopPropagation();\n setRotateTarget(key);\n }}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n \"text-status-info-text hover:bg-status-info-bg-subtle\",\n \"disabled:opacity-50 disabled:cursor-not-allowed\",\n )}\n >\n <RotateCw size={14} />\n <span className=\"sr-only sm:not-sr-only\">Rotate</span>\n </button>\n <button\n type=\"button\"\n title=\"Revoke key\"\n disabled={revokeLoading}\n onClick={(e) => {\n e.stopPropagation();\n setRevokeTarget(key);\n }}\n className={cn(\n \"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors\",\n \"text-status-error-text hover:bg-status-error-bg-subtle\",\n \"disabled:opacity-50 disabled:cursor-not-allowed\",\n )}\n >\n <Ban size={14} />\n <span className=\"sr-only sm:not-sr-only\">Revoke</span>\n </button>\n </div>\n );\n },\n },\n ];\n\n // Stats\n const stats = {\n total: apiKeys.length,\n active: apiKeys.filter((k) => !isExpired(k.expiresAt)).length,\n expiringSoon: apiKeys.filter((k) => isExpiringSoon(k.expiresAt)).length,\n expired: apiKeys.filter((k) => isExpired(k.expiresAt)).length,\n };\n\n const handleBulkRevoke = async () => {\n if (selectedActiveKeys.length === 0) return;\n\n const results = await Promise.allSettled(\n selectedActiveKeys.map((key) =>\n revokeApiKey({ variables: { keyId: key.id } }),\n ),\n );\n\n const succeeded = results.filter(\n (result) => result.status === \"fulfilled\",\n ).length;\n const failed = results.length - succeeded;\n\n setBulkFeedback({\n type: failed === 0 ? \"success\" : \"error\",\n message: `Revoked ${succeeded} API key${succeeded === 1 ? \"\" : \"s\"}${\n failed > 0 ? ` • ${failed} failed` : \"\"\n }`,\n });\n setShowBulkRevokeDialog(false);\n clearSelection();\n await refetch();\n };\n\n return (\n <PermissionGuard permission={DEVPORTAL_PERMISSIONS.APP_READ}>\n <PageLayout\n title={tr(\"devportal.apiKeys\", \"API Keys\")}\n description={tr(\n \"devportal.manageApiKeys\",\n \"Manage your API keys for programmatic access\",\n )}\n actions={\n <button\n onClick={() => handleNavigate(\"/api-keys/new\")}\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 <Plus size={16} />\n {tr(\"devportal.generateApiKey\", \"Generate API Key\")}\n </button>\n }\n >\n {/* New secret alert — shown after a successful rotation */}\n {newSecret && (\n <NewSecretAlert\n title=\"API Key Rotated\"\n secret={newSecret}\n description=\"The previous secret has been invalidated. Copy the new secret below — it will not be shown again.\"\n onDismiss={() => setNewSecret(null)}\n />\n )}\n\n {bulkFeedback && (\n <div\n className={cn(\n \"rounded-lg border px-4 py-3 text-sm\",\n bulkFeedback.type === \"success\"\n ? \"border-border-subtle bg-status-success-bg-subtle text-status-success-text\"\n : \"border-destructive/40 bg-destructive/10 text-destructive\",\n )}\n >\n <div className=\"flex items-center justify-between gap-3\">\n <span>{bulkFeedback.message}</span>\n <button\n onClick={() => setBulkFeedback(null)}\n className=\"text-xs font-medium underline underline-offset-2\"\n >\n Dismiss\n </button>\n </div>\n </div>\n )}\n\n {/* Filters */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex flex-1 items-center gap-4\">\n {/* Search */}\n <div className=\"relative flex-1 max-w-md\">\n <Search\n size={16}\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n />\n <input\n type=\"text\"\n placeholder=\"Search by name, prefix, or app...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-md border bg-background pl-9 pr-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n />\n </div>\n\n {/* Status Filter */}\n <select\n value={statusFilter}\n onChange={(e) =>\n setStatusFilter(e.target.value as \"all\" | \"active\" | \"expired\")\n }\n className=\"rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"all\">All Status</option>\n <option value=\"active\">Active</option>\n <option value=\"expired\">Expired</option>\n </select>\n </div>\n </div>\n\n {/* Summary Stats */}\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"flex items-center gap-2 text-muted-foreground\">\n <Key size={16} />\n <span className=\"text-sm\">Total Keys</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold\">{stats.total}</p>\n </div>\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"flex items-center gap-2 text-status-success-text\">\n <Shield size={16} />\n <span className=\"text-sm\">Active</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold\">{stats.active}</p>\n </div>\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"flex items-center gap-2 text-status-warning-text\">\n <Clock size={16} />\n <span className=\"text-sm\">Expiring Soon</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold\">{stats.expiringSoon}</p>\n </div>\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"flex items-center gap-2 text-status-error-text\">\n <AlertTriangle size={16} />\n <span className=\"text-sm\">Expired</span>\n </div>\n <p className=\"mt-1 text-2xl font-semibold\">{stats.expired}</p>\n </div>\n </div>\n\n {/* Results count */}\n <p className=\"text-sm text-muted-foreground\">\n {filteredKeys.length} API key{filteredKeys.length !== 1 ? \"s\" : \"\"}{\" \"}\n found\n </p>\n\n {filteredKeys.length > 0 && (\n <div className=\"rounded-lg border bg-card p-4\">\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"flex items-center gap-2\">\n <SelectionCheckbox\n checked={allVisibleSelected}\n indeterminate={partiallySelected}\n onChange={toggleAllVisibleKeys}\n ariaLabel=\"Select all visible API keys\"\n />\n <span className=\"text-sm font-medium\">\n Select all active keys\n </span>\n </div>\n {selectedKeyIds.size > 0 && (\n <button\n onClick={clearSelection}\n className=\"text-sm text-muted-foreground hover:text-foreground\"\n >\n Clear selection\n </button>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-full bg-muted px-3 py-1 text-sm font-medium text-muted-foreground\">\n {selectedKeyIds.size} selected\n </span>\n <button\n onClick={() => setShowBulkRevokeDialog(true)}\n disabled={selectedActiveKeys.length === 0}\n className=\"inline-flex items-center gap-2 rounded-md border border-status-error-bg px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <Ban size={16} />\n Revoke Selected\n {selectedActiveKeys.length > 0\n ? ` (${selectedActiveKeys.length})`\n : \"\"}\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* Keys Table */}\n {filteredKeys.length > 0 ? (\n <DataTable data={filteredKeys} columns={columns} keyField=\"id\" />\n ) : (\n <EmptyState\n icon={<Key size={32} />}\n title={tr(\"devportal.noApiKeysFound\", \"No API keys found\")}\n description={\n searchQuery || statusFilter !== \"all\"\n ? tr(\n \"devportal.tryAdjustingFilters\",\n \"Try adjusting your filters\",\n )\n : tr(\n \"devportal.generateFirstApiKey\",\n \"Generate your first API key to start making API calls\",\n )\n }\n action={\n !searchQuery && statusFilter === \"all\" ? (\n <button\n onClick={() => handleNavigate(\"/api-keys/new\")}\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\"\n >\n <Plus size={16} />\n {tr(\"devportal.generateApiKey\", \"Generate API Key\")}\n </button>\n ) : undefined\n }\n />\n )}\n\n {/* Revoke confirmation dialog */}\n <ConfirmDialog\n open={revokeTarget !== null}\n onOpenChange={(open) => {\n if (!open) setRevokeTarget(null);\n }}\n onConfirm={handleRevoke}\n title=\"Revoke API Key\"\n description={`Are you sure you want to revoke \"${revokeTarget?.name ?? \"\"}\"? This action cannot be undone. Any application or service using this key will immediately lose access.`}\n confirmLabel=\"Revoke Key\"\n cancelLabel=\"Cancel\"\n variant=\"danger\"\n loading={revokeLoading}\n />\n\n {/* Rotate confirmation dialog */}\n <ConfirmDialog\n open={rotateTarget !== null}\n onOpenChange={(open) => {\n if (!open) setRotateTarget(null);\n }}\n onConfirm={handleRotate}\n title=\"Rotate API Key Secret\"\n description={`This will generate a new secret for \"${rotateTarget?.name ?? \"\"}\" and immediately invalidate the current one. Make sure to update all services using this key before the old secret stops working.`}\n confirmLabel=\"Rotate Secret\"\n cancelLabel=\"Cancel\"\n variant=\"warning\"\n loading={rotateLoading}\n />\n\n <ConfirmDialog\n open={showBulkRevokeDialog}\n onOpenChange={setShowBulkRevokeDialog}\n onConfirm={handleBulkRevoke}\n title=\"Revoke selected API keys\"\n description={`Revoke ${selectedActiveKeys.length} selected API key${selectedActiveKeys.length === 1 ? \"\" : \"s\"}? Any service using these keys will immediately lose access.`}\n confirmLabel=\"Revoke Keys\"\n cancelLabel=\"Cancel\"\n variant=\"danger\"\n loading={revokeLoading}\n />\n </PageLayout>\n </PermissionGuard>\n );\n};\n"],"mappings":"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"}
@@ -32,7 +32,7 @@ var w = () => {
32
32
  let { t: S } = h(), w = (e, t) => {
33
33
  let n = S(e);
34
34
  return n === e ? t : n;
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
+ }, { 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?.apps.map((e) => ({
36
36
  id: e.id,
37
37
  name: e.name,
38
38
  clientId: e.clientId
@@ -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 { 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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"}
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?.apps.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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"}
@@ -95,7 +95,7 @@ var C = Object.entries(i).map(([e, t]) => ({
95
95
  }))], R = [{
96
96
  value: "",
97
97
  label: O("devportal.noLinkedAuthApp", "Link later")
98
- }, ...(F?.currentUserApps ?? []).map((e) => ({
98
+ }, ...(F?.currentUserApps?.apps ?? []).map((e) => ({
99
99
  value: e.id,
100
100
  label: `${e.name} · ${e.type} · ${e.clientId}`
101
101
  })) ?? []], [z, B] = h({