@lukoweb/apitogo-plugin-dev-portal-billing 0.1.65 → 0.1.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,10 +1,39 @@
1
1
  import * as react_jsx_runtime0 from "react/jsx-runtime";
2
2
  import * as _lukoweb_apitogo_plugins0 from "@lukoweb/apitogo/plugins";
3
+ import * as _tanstack_react_query0 from "@tanstack/react-query";
3
4
 
4
5
  //#region src/pages/AccountPlansPage.d.ts
5
6
  declare const AccountPlansPage: () => react_jsx_runtime0.JSX.Element;
6
7
  //#endregion
8
+ //#region src/components/ApiKeysPanel.d.ts
9
+ /**
10
+ * Lists the developer's gateway API keys with create and revoke. The plaintext key is returned by the
11
+ * platform exactly once, so it is held in local state until dismissed and never re-fetched.
12
+ */
13
+ declare const ApiKeysPanel: ({
14
+ readOnly
15
+ }: {
16
+ readOnly?: boolean;
17
+ }) => react_jsx_runtime0.JSX.Element;
18
+ //#endregion
7
19
  //#region src/types.d.ts
20
+ type DevPortalApiKey = {
21
+ id: string;
22
+ name: string;
23
+ keyPrefix: string;
24
+ status: string;
25
+ createdAtUtc: string;
26
+ revokedAtUtc: string | null; /** Present only in the response to a successful create; never returned again. */
27
+ key: string | null;
28
+ };
29
+ type DevPortalApiKeyListResponse = {
30
+ keys: DevPortalApiKey[];
31
+ activeCount: number; /** Per-developer cap from the project's platform plan; null means unlimited. */
32
+ maxKeys: number | null;
33
+ canCreate: boolean; /** Header the key must be sent in when calling the gateway. */
34
+ header: string;
35
+ gatewayBaseUrl: string | null;
36
+ };
8
37
  type DevPortalBillingConfig = {
9
38
  pricing?: {
10
39
  title?: string;
@@ -30,4 +59,119 @@ declare const CREDENTIALS_PATH = "/account/credentials";
30
59
  //#region src/DevPortalBillingPlugin.d.ts
31
60
  declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) => _lukoweb_apitogo_plugins0.ApitogoPlugin;
32
61
  //#endregion
33
- export { AccountPlansPage, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
62
+ //#region src/hooks/useApiKeys.d.ts
63
+ /** Thrown when the developer has hit their plan's per-developer key cap. */
64
+ declare const API_KEY_CAP_REACHED = "API_KEY_CAP_REACHED";
65
+ declare function useApiKeys(): _tanstack_react_query0.QueryObserverRefetchErrorResult<DevPortalApiKeyListResponse, Error> | _tanstack_react_query0.QueryObserverLoadingErrorResult<DevPortalApiKeyListResponse, Error> | _tanstack_react_query0.QueryObserverPendingResult<DevPortalApiKeyListResponse, Error> | {
66
+ data: DevPortalApiKeyListResponse;
67
+ error: null;
68
+ isError: boolean;
69
+ isFetching: boolean;
70
+ isLoading: boolean;
71
+ isPending: boolean;
72
+ isSuccess: boolean;
73
+ status: string;
74
+ isLoadingError: false;
75
+ isRefetchError: true;
76
+ isPlaceholderData: false;
77
+ dataUpdatedAt: number;
78
+ errorUpdatedAt: number;
79
+ failureCount: number;
80
+ failureReason: Error | null;
81
+ errorUpdateCount: number;
82
+ isFetched: boolean;
83
+ isFetchedAfterMount: boolean;
84
+ isInitialLoading: boolean;
85
+ isPaused: boolean;
86
+ isRefetching: boolean;
87
+ isStale: boolean;
88
+ isEnabled: boolean;
89
+ refetch: (options?: _tanstack_react_query0.RefetchOptions) => Promise<_tanstack_react_query0.QueryObserverResult<DevPortalApiKeyListResponse, Error>>;
90
+ fetchStatus: _tanstack_react_query0.FetchStatus;
91
+ promise: Promise<DevPortalApiKeyListResponse>;
92
+ } | {
93
+ data: DevPortalApiKeyListResponse;
94
+ error: null;
95
+ isError: boolean;
96
+ isFetching: boolean;
97
+ isLoading: boolean;
98
+ isPending: boolean;
99
+ isSuccess: boolean;
100
+ status: string;
101
+ isLoadingError: false;
102
+ isRefetchError: false;
103
+ isPlaceholderData: false;
104
+ dataUpdatedAt: number;
105
+ errorUpdatedAt: number;
106
+ failureCount: number;
107
+ failureReason: Error | null;
108
+ errorUpdateCount: number;
109
+ isFetched: boolean;
110
+ isFetchedAfterMount: boolean;
111
+ isInitialLoading: boolean;
112
+ isPaused: boolean;
113
+ isRefetching: boolean;
114
+ isStale: boolean;
115
+ isEnabled: boolean;
116
+ refetch: (options?: _tanstack_react_query0.RefetchOptions) => Promise<_tanstack_react_query0.QueryObserverResult<DevPortalApiKeyListResponse, Error>>;
117
+ fetchStatus: _tanstack_react_query0.FetchStatus;
118
+ promise: Promise<DevPortalApiKeyListResponse>;
119
+ } | {
120
+ data: DevPortalApiKeyListResponse;
121
+ error: null;
122
+ isError: boolean;
123
+ isFetching: boolean;
124
+ isLoading: boolean;
125
+ isPending: boolean;
126
+ isSuccess: boolean;
127
+ status: string;
128
+ isLoadingError: true;
129
+ isRefetchError: false;
130
+ isPlaceholderData: false;
131
+ dataUpdatedAt: number;
132
+ errorUpdatedAt: number;
133
+ failureCount: number;
134
+ failureReason: Error | null;
135
+ errorUpdateCount: number;
136
+ isFetched: boolean;
137
+ isFetchedAfterMount: boolean;
138
+ isInitialLoading: boolean;
139
+ isPaused: boolean;
140
+ isRefetching: boolean;
141
+ isStale: boolean;
142
+ isEnabled: boolean;
143
+ refetch: (options?: _tanstack_react_query0.RefetchOptions) => Promise<_tanstack_react_query0.QueryObserverResult<DevPortalApiKeyListResponse, Error>>;
144
+ fetchStatus: _tanstack_react_query0.FetchStatus;
145
+ promise: Promise<DevPortalApiKeyListResponse>;
146
+ } | {
147
+ data: DevPortalApiKeyListResponse;
148
+ error: null;
149
+ isError: boolean;
150
+ isFetching: boolean;
151
+ isLoading: boolean;
152
+ isPending: boolean;
153
+ isSuccess: boolean;
154
+ status: string;
155
+ isLoadingError: false;
156
+ isRefetchError: false;
157
+ isPlaceholderData: true;
158
+ dataUpdatedAt: number;
159
+ errorUpdatedAt: number;
160
+ failureCount: number;
161
+ failureReason: Error | null;
162
+ errorUpdateCount: number;
163
+ isFetched: boolean;
164
+ isFetchedAfterMount: boolean;
165
+ isInitialLoading: boolean;
166
+ isPaused: boolean;
167
+ isRefetching: boolean;
168
+ isStale: boolean;
169
+ isEnabled: boolean;
170
+ refetch: (options?: _tanstack_react_query0.RefetchOptions) => Promise<_tanstack_react_query0.QueryObserverResult<DevPortalApiKeyListResponse, Error>>;
171
+ fetchStatus: _tanstack_react_query0.FetchStatus;
172
+ promise: Promise<DevPortalApiKeyListResponse>;
173
+ };
174
+ declare function useCreateApiKey(): _tanstack_react_query0.UseMutationResult<DevPortalApiKey, Error, string, unknown>;
175
+ declare function useRevokeApiKey(): _tanstack_react_query0.UseMutationResult<void, Error, string, unknown>;
176
+ //#endregion
177
+ export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Button, Head, Heading } from "@lukoweb/apitogo/components";
2
- import { CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon } from "@lukoweb/apitogo/icons";
2
+ import { CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon, TrashIcon } from "@lukoweb/apitogo/icons";
3
3
  import { createContext, useContext, useEffect, useState } from "react";
4
4
  import { Link, Navigate, Outlet, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
5
5
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog.js";
@@ -9,10 +9,14 @@ import zudokuConfig from "virtual:zudoku-config";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
10
  import { useMutation, useQuery, useQueryClient } from "@lukoweb/apitogo/react-query";
11
11
  import { useAuth } from "@lukoweb/apitogo/hooks";
12
- import { createPlugin } from "@lukoweb/apitogo/plugins";
13
- import { Alert, AlertAction, AlertDescription } from "@lukoweb/apitogo/ui/Alert";
14
- import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button";
12
+ import { Alert, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
13
+ import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
14
+ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@lukoweb/apitogo/ui/Dialog.js";
15
+ import { Input } from "@lukoweb/apitogo/ui/Input.js";
15
16
  import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
17
+ import { createPlugin } from "@lukoweb/apitogo/plugins";
18
+ import { Alert as Alert$1, AlertAction, AlertDescription } from "@lukoweb/apitogo/ui/Alert";
19
+ import { Button as Button$2 } from "@lukoweb/apitogo/ui/Button";
16
20
  import { Value } from "@lukoweb/apitogo/ui/Value.js";
17
21
 
18
22
  //#region src/utils/dev-portal-api.ts
@@ -286,6 +290,8 @@ function useCancelSubscription() {
286
290
  //#endregion
287
291
  //#region src/utils/demo-session.ts
288
292
  const DEMO_SUBSCRIPTION_KEY = "demo-subscription-key";
293
+ const DEMO_GATEWAY_KEY_HEADER = "Ocp-Apim-Subscription-Key";
294
+ const DEMO_MAX_KEYS = 5;
289
295
  function resolveDemoPlanSku(plans) {
290
296
  return plans?.[0]?.sku ?? "free";
291
297
  }
@@ -314,6 +320,24 @@ function createDemoCredentials(plans) {
314
320
  expiresAt: null
315
321
  };
316
322
  }
323
+ function createDemoApiKeys() {
324
+ return {
325
+ keys: [{
326
+ id: "00000000-0000-0000-0000-0000000000a1",
327
+ name: "default",
328
+ keyPrefix: DEMO_SUBSCRIPTION_KEY.slice(0, 8),
329
+ status: "active",
330
+ createdAtUtc: (/* @__PURE__ */ new Date()).toISOString(),
331
+ revokedAtUtc: null,
332
+ key: null
333
+ }],
334
+ activeCount: 1,
335
+ maxKeys: DEMO_MAX_KEYS,
336
+ canCreate: true,
337
+ header: DEMO_GATEWAY_KEY_HEADER,
338
+ gatewayBaseUrl: null
339
+ };
340
+ }
317
341
 
318
342
  //#endregion
319
343
  //#region src/hooks/useMySubscriptions.ts
@@ -467,6 +491,259 @@ const AccountPlansPage = () => {
467
491
  });
468
492
  };
469
493
 
494
+ //#endregion
495
+ //#region src/hooks/useApiKeys.ts
496
+ /** Thrown when the developer has hit their plan's per-developer key cap. */
497
+ const API_KEY_CAP_REACHED = "API_KEY_CAP_REACHED";
498
+ function apiKeysQueryKey(apiBaseUrl) {
499
+ return ["dev-portal-api-keys", apiBaseUrl];
500
+ }
501
+ function useApiKeys() {
502
+ const auth = useAuth();
503
+ const apiBaseUrl = useDevPortalApiBaseUrl();
504
+ const isPreview = useDevPortalPreviewMode();
505
+ const isDemoSession = useIsDemoSession();
506
+ const liveQuery = useQuery({
507
+ queryKey: apiKeysQueryKey(apiBaseUrl),
508
+ enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
509
+ queryFn: async () => {
510
+ const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
511
+ method: "GET",
512
+ headers: { Accept: "application/json" },
513
+ credentials: "include"
514
+ });
515
+ if (response.status === 404) throw new Error("NO_ENTITLEMENT");
516
+ if (!response.ok) throw new Error("Failed to load API keys.");
517
+ return await response.json();
518
+ },
519
+ retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
520
+ });
521
+ if (isDemoSession && auth.isAuthenticated) return {
522
+ ...liveQuery,
523
+ data: createDemoApiKeys(),
524
+ error: null,
525
+ isError: false,
526
+ isFetching: false,
527
+ isLoading: false,
528
+ isPending: false,
529
+ isSuccess: true,
530
+ status: "success"
531
+ };
532
+ return liveQuery;
533
+ }
534
+ function useCreateApiKey() {
535
+ const apiBaseUrl = useDevPortalApiBaseUrl();
536
+ const queryClient = useQueryClient();
537
+ return useMutation({
538
+ mutationFn: async (name) => {
539
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
540
+ const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
541
+ method: "POST",
542
+ headers: {
543
+ Accept: "application/json",
544
+ "Content-Type": "application/json"
545
+ },
546
+ credentials: "include",
547
+ body: JSON.stringify({ name })
548
+ });
549
+ if (response.status === 409) throw new Error(API_KEY_CAP_REACHED);
550
+ if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Failed to create API key.");
551
+ return await response.json();
552
+ },
553
+ onSuccess: async () => {
554
+ await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
555
+ }
556
+ });
557
+ }
558
+ function useRevokeApiKey() {
559
+ const apiBaseUrl = useDevPortalApiBaseUrl();
560
+ const queryClient = useQueryClient();
561
+ return useMutation({
562
+ mutationFn: async (keyId) => {
563
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
564
+ const response = await fetch(`${apiBaseUrl}/api/v1/api-keys/${keyId}`, {
565
+ method: "DELETE",
566
+ headers: { Accept: "application/json" },
567
+ credentials: "include"
568
+ });
569
+ if (!response.ok && response.status !== 204) throw new Error(await readProblemDetail(response) ?? "Failed to revoke API key.");
570
+ },
571
+ onSuccess: async () => {
572
+ await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
573
+ }
574
+ });
575
+ }
576
+ async function readProblemDetail(response) {
577
+ try {
578
+ return (await response.json()).detail?.trim() || null;
579
+ } catch {
580
+ return null;
581
+ }
582
+ }
583
+
584
+ //#endregion
585
+ //#region src/components/ApiKeysPanel.tsx
586
+ /**
587
+ * Lists the developer's gateway API keys with create and revoke. The plaintext key is returned by the
588
+ * platform exactly once, so it is held in local state until dismissed and never re-fetched.
589
+ */
590
+ const ApiKeysPanel = ({ readOnly = false }) => {
591
+ const { data, isLoading, error } = useApiKeys();
592
+ const createKey = useCreateApiKey();
593
+ const revokeKey = useRevokeApiKey();
594
+ const [name, setName] = useState("");
595
+ const [newKey, setNewKey] = useState(null);
596
+ const activeKeys = data?.keys.filter((key) => key.status === "active") ?? [];
597
+ const capLabel = data?.maxKeys == null ? `${data?.activeCount ?? activeKeys.length} active` : `${data.activeCount} of ${data.maxKeys} used`;
598
+ const canCreate = !readOnly && (data?.canCreate ?? false);
599
+ const handleCreate = () => {
600
+ const trimmed = name.trim();
601
+ if (!trimmed) return;
602
+ createKey.mutate(trimmed, { onSuccess: (key) => {
603
+ setNewKey(key);
604
+ setName("");
605
+ } });
606
+ };
607
+ return /* @__PURE__ */ jsxs("article", {
608
+ className: "rounded-xl border bg-card/60 p-6 space-y-5",
609
+ children: [
610
+ /* @__PURE__ */ jsxs("div", {
611
+ className: "flex flex-wrap items-start justify-between gap-3",
612
+ children: [/* @__PURE__ */ jsxs("div", {
613
+ className: "space-y-1",
614
+ children: [/* @__PURE__ */ jsx("h2", {
615
+ className: "text-lg font-medium",
616
+ children: "API keys"
617
+ }), /* @__PURE__ */ jsxs("p", {
618
+ className: "text-sm text-muted-foreground",
619
+ children: [
620
+ "Send a key in the",
621
+ " ",
622
+ /* @__PURE__ */ jsx("code", {
623
+ className: "rounded bg-muted px-1 py-0.5 text-xs",
624
+ children: data?.header ?? "Ocp-Apim-Subscription-Key"
625
+ }),
626
+ " ",
627
+ "header when calling the gateway. Keys share your plan's rate limit and quota."
628
+ ]
629
+ })]
630
+ }), data ? /* @__PURE__ */ jsx("span", {
631
+ className: "shrink-0 rounded-full border px-3 py-1 text-xs text-muted-foreground",
632
+ children: capLabel
633
+ }) : null]
634
+ }),
635
+ newKey?.key ? /* @__PURE__ */ jsxs(Alert, { children: [
636
+ /* @__PURE__ */ jsx(KeyRoundIcon, { size: 16 }),
637
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Copy your new key now — it is shown only once." }),
638
+ /* @__PURE__ */ jsxs("div", {
639
+ className: "mt-3 space-y-3",
640
+ children: [/* @__PURE__ */ jsx(Secret, {
641
+ className: "max-w-full w-full",
642
+ secret: newKey.key,
643
+ status: "active"
644
+ }), /* @__PURE__ */ jsx(Button$1, {
645
+ variant: "outline",
646
+ onClick: () => setNewKey(null),
647
+ children: "Done"
648
+ })]
649
+ })
650
+ ] }) : null,
651
+ createKey.isError ? /* @__PURE__ */ jsx(Alert, {
652
+ variant: "destructive",
653
+ children: /* @__PURE__ */ jsx(AlertTitle, { children: createKey.error.message === API_KEY_CAP_REACHED ? data?.maxKeys == null ? "You cannot create more API keys right now." : `You already have the maximum of ${data.maxKeys} active API keys. Revoke one to create another.` : createKey.error.message })
654
+ }) : null,
655
+ revokeKey.isError ? /* @__PURE__ */ jsx(Alert, {
656
+ variant: "destructive",
657
+ children: /* @__PURE__ */ jsx(AlertTitle, { children: revokeKey.error.message })
658
+ }) : null,
659
+ isLoading ? /* @__PURE__ */ jsx("p", {
660
+ className: "text-sm text-muted-foreground",
661
+ children: "Loading…"
662
+ }) : null,
663
+ error ? /* @__PURE__ */ jsx("p", {
664
+ className: "text-sm text-muted-foreground",
665
+ children: error instanceof Error && error.message === "NO_ENTITLEMENT" ? "Choose a plan to get your first API key." : "Unable to load your API keys. Try again in a moment."
666
+ }) : null,
667
+ data && activeKeys.length === 0 && !isLoading ? /* @__PURE__ */ jsx("p", {
668
+ className: "text-sm text-muted-foreground",
669
+ children: "You have no active API keys yet."
670
+ }) : null,
671
+ activeKeys.length > 0 ? /* @__PURE__ */ jsx("ul", {
672
+ className: "divide-y rounded-lg border",
673
+ children: activeKeys.map((key) => /* @__PURE__ */ jsxs("li", {
674
+ className: "flex flex-wrap items-center justify-between gap-3 p-4",
675
+ children: [/* @__PURE__ */ jsxs("div", {
676
+ className: "min-w-0 space-y-1",
677
+ children: [/* @__PURE__ */ jsx("p", {
678
+ className: "truncate font-medium",
679
+ children: key.name
680
+ }), /* @__PURE__ */ jsxs("p", {
681
+ className: "text-xs text-muted-foreground",
682
+ children: [
683
+ /* @__PURE__ */ jsxs("code", { children: [key.keyPrefix, "…"] }),
684
+ " · created",
685
+ " ",
686
+ new Date(key.createdAtUtc).toLocaleDateString()
687
+ ]
688
+ })]
689
+ }), readOnly ? null : /* @__PURE__ */ jsxs(Dialog, { children: [/* @__PURE__ */ jsx(DialogTrigger, {
690
+ asChild: true,
691
+ children: /* @__PURE__ */ jsxs(Button$1, {
692
+ variant: "ghost",
693
+ className: "flex items-center gap-2",
694
+ disabled: revokeKey.isPending,
695
+ children: [/* @__PURE__ */ jsx(TrashIcon, { size: 16 }), /* @__PURE__ */ jsx("span", {
696
+ className: "hidden md:block",
697
+ children: "Revoke"
698
+ })]
699
+ })
700
+ }), /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: [
701
+ "Revoke “",
702
+ key.name,
703
+ "”?"
704
+ ] }), /* @__PURE__ */ jsx(DialogDescription, { children: "Any application using this key will immediately start getting 401 responses from the gateway. This cannot be undone." })] }), /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(DialogClose, {
705
+ asChild: true,
706
+ children: /* @__PURE__ */ jsx(Button$1, {
707
+ variant: "outline",
708
+ children: "Cancel"
709
+ })
710
+ }), /* @__PURE__ */ jsx(DialogClose, {
711
+ asChild: true,
712
+ children: /* @__PURE__ */ jsx(Button$1, {
713
+ variant: "destructive",
714
+ onClick: () => revokeKey.mutate(key.id),
715
+ children: "Revoke key"
716
+ })
717
+ })] })] })] })]
718
+ }, key.id))
719
+ }) : null,
720
+ readOnly ? null : /* @__PURE__ */ jsxs("div", {
721
+ className: "flex flex-wrap items-center gap-2",
722
+ children: [/* @__PURE__ */ jsx(Input, {
723
+ className: "max-w-xs",
724
+ maxLength: 64,
725
+ placeholder: "Key name (e.g. production)",
726
+ "aria-label": "New API key name",
727
+ value: name,
728
+ disabled: !canCreate || createKey.isPending,
729
+ onChange: (event) => setName(event.target.value),
730
+ onKeyDown: (event) => {
731
+ if (event.key === "Enter") handleCreate();
732
+ }
733
+ }), /* @__PURE__ */ jsx(Button$1, {
734
+ onClick: handleCreate,
735
+ disabled: !canCreate || createKey.isPending || !name.trim(),
736
+ children: createKey.isPending ? "Creating…" : "Create API key"
737
+ })]
738
+ }),
739
+ data && !data.canCreate && !readOnly ? /* @__PURE__ */ jsx("p", {
740
+ className: "text-sm text-muted-foreground",
741
+ children: data.maxKeys == null ? "API key creation is currently disabled for this API." : `You have reached your plan limit of ${data.maxKeys} API keys. Revoke a key to create a new one.`
742
+ }) : null
743
+ ]
744
+ });
745
+ };
746
+
470
747
  //#endregion
471
748
  //#region src/DevPortalBillingWrapper.tsx
472
749
  const DevPortalBillingWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(DevPortalBillingProvider, {
@@ -589,7 +866,7 @@ const CheckoutPage = () => {
589
866
  className: "text-muted-foreground",
590
867
  children: "Checkout is simulated in local demo mode. Publish your portal to APItoGo to complete real subscriptions and billing."
591
868
  }),
592
- /* @__PURE__ */ jsx(Button$1, {
869
+ /* @__PURE__ */ jsx(Button$2, {
593
870
  asChild: true,
594
871
  variant: "outline",
595
872
  children: /* @__PURE__ */ jsx(Link, {
@@ -610,7 +887,7 @@ const CheckoutPage = () => {
610
887
  className: "text-muted-foreground",
611
888
  children: "Subscribe and billing flows require a live dev-portal backend. Publish your portal to APItoGo, then return here to complete checkout."
612
889
  }),
613
- /* @__PURE__ */ jsx(Button$1, {
890
+ /* @__PURE__ */ jsx(Button$2, {
614
891
  asChild: true,
615
892
  variant: "outline",
616
893
  children: /* @__PURE__ */ jsx(Link, {
@@ -626,12 +903,12 @@ const CheckoutPage = () => {
626
903
  title: "Establishing secure checkout…",
627
904
  description: "Redirecting you to complete your subscription.",
628
905
  url: checkoutQuery.data?.checkoutUrl ?? void 0,
629
- children: checkoutQuery.isError ? /* @__PURE__ */ jsxs(Alert, {
906
+ children: checkoutQuery.isError ? /* @__PURE__ */ jsxs(Alert$1, {
630
907
  variant: "destructive",
631
908
  children: [/* @__PURE__ */ jsx(AlertDescription, {
632
909
  className: "first-letter:uppercase",
633
910
  children: checkoutQuery.error.message
634
- }), /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button$1, {
911
+ }), /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button$2, {
635
912
  variant: "outline",
636
913
  size: "xs",
637
914
  asChild: true,
@@ -707,7 +984,7 @@ const CredentialsPage = () => {
707
984
  children: "API credentials"
708
985
  }), /* @__PURE__ */ jsx("p", {
709
986
  className: "text-muted-foreground",
710
- children: "Use your subscription key to authenticate requests to the API gateway."
987
+ children: "Create API keys and use them to authenticate requests to the API gateway."
711
988
  })]
712
989
  })]
713
990
  }),
@@ -727,7 +1004,7 @@ const CredentialsPage = () => {
727
1004
  children: [/* @__PURE__ */ jsx("p", {
728
1005
  className: "text-muted-foreground",
729
1006
  children: "Sign in to view your subscription key."
730
- }), /* @__PURE__ */ jsx(Button$1, {
1007
+ }), /* @__PURE__ */ jsx(Button$2, {
731
1008
  className: "mt-4",
732
1009
  asChild: true,
733
1010
  children: /* @__PURE__ */ jsx(Link, {
@@ -745,7 +1022,7 @@ const CredentialsPage = () => {
745
1022
  children: [/* @__PURE__ */ jsx("p", {
746
1023
  className: "text-muted-foreground",
747
1024
  children: error instanceof Error && error.message === "NO_ENTITLEMENT" ? "No active subscription found. Choose a plan to get your API key." : "Unable to load API credentials. Try again in a moment."
748
- }), /* @__PURE__ */ jsx(Button$1, {
1025
+ }), /* @__PURE__ */ jsx(Button$2, {
749
1026
  className: "mt-4",
750
1027
  asChild: true,
751
1028
  children: /* @__PURE__ */ jsx(Link, {
@@ -770,24 +1047,20 @@ const CredentialsPage = () => {
770
1047
  })]
771
1048
  })
772
1049
  }),
1050
+ /* @__PURE__ */ jsx(ApiKeysPanel, { readOnly: isDemoSession }),
773
1051
  /* @__PURE__ */ jsxs("article", {
774
1052
  className: "rounded-xl border bg-card/60 p-6 space-y-4",
775
1053
  children: [/* @__PURE__ */ jsxs("div", {
776
1054
  className: "space-y-1",
777
1055
  children: [/* @__PURE__ */ jsx("h2", {
778
1056
  className: "text-lg font-medium",
779
- children: "Subscription key"
1057
+ children: "Default key value"
780
1058
  }), /* @__PURE__ */ jsxs("p", {
781
1059
  className: "text-sm text-muted-foreground",
782
1060
  children: [
783
- "Send this value in the",
784
- " ",
785
- /* @__PURE__ */ jsx("code", {
786
- className: "rounded bg-muted px-1 py-0.5 text-xs",
787
- children: "Ocp-Apim-Subscription-Key"
788
- }),
789
- " ",
790
- "header when calling the gateway."
1061
+ "The full value of the ",
1062
+ /* @__PURE__ */ jsx("code", { children: "default" }),
1063
+ " key issued with your plan. Keys you create above are shown only once, so store them yourself."
791
1064
  ]
792
1065
  })]
793
1066
  }), /* @__PURE__ */ jsx(Secret, {
@@ -821,14 +1094,14 @@ const CredentialsPage = () => {
821
1094
  }) : null,
822
1095
  /* @__PURE__ */ jsxs("div", {
823
1096
  className: "mt-8 flex flex-wrap gap-3",
824
- children: [/* @__PURE__ */ jsx(Button$1, {
1097
+ children: [/* @__PURE__ */ jsx(Button$2, {
825
1098
  variant: "outline",
826
1099
  asChild: true,
827
1100
  children: /* @__PURE__ */ jsx(Link, {
828
1101
  to: "/account/subscriptions",
829
1102
  children: "My subscriptions"
830
1103
  })
831
- }), /* @__PURE__ */ jsx(Button$1, {
1104
+ }), /* @__PURE__ */ jsx(Button$2, {
832
1105
  variant: "outline",
833
1106
  asChild: true,
834
1107
  children: /* @__PURE__ */ jsx(Link, {
@@ -975,7 +1248,7 @@ const SubscriptionsPage = () => {
975
1248
  children: [/* @__PURE__ */ jsx("p", {
976
1249
  className: "text-muted-foreground",
977
1250
  children: "You do not have an active subscription yet."
978
- }), /* @__PURE__ */ jsx(Button$1, {
1251
+ }), /* @__PURE__ */ jsx(Button$2, {
979
1252
  className: "mt-4",
980
1253
  asChild: true,
981
1254
  children: /* @__PURE__ */ jsx(Link, {
@@ -1021,7 +1294,7 @@ const SubscriptionsPage = () => {
1021
1294
  /* @__PURE__ */ jsxs("div", {
1022
1295
  className: "mt-8 flex flex-wrap gap-3",
1023
1296
  children: [
1024
- /* @__PURE__ */ jsx(Button$1, {
1297
+ /* @__PURE__ */ jsx(Button$2, {
1025
1298
  variant: "outline",
1026
1299
  asChild: true,
1027
1300
  children: /* @__PURE__ */ jsx(Link, {
@@ -1029,7 +1302,7 @@ const SubscriptionsPage = () => {
1029
1302
  children: "Change plan"
1030
1303
  })
1031
1304
  }),
1032
- /* @__PURE__ */ jsx(Button$1, {
1305
+ /* @__PURE__ */ jsx(Button$2, {
1033
1306
  variant: "outline",
1034
1307
  asChild: true,
1035
1308
  children: /* @__PURE__ */ jsx(Link, {
@@ -1037,7 +1310,7 @@ const SubscriptionsPage = () => {
1037
1310
  children: "Billing settings"
1038
1311
  })
1039
1312
  }),
1040
- auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
1313
+ auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$2, {
1041
1314
  variant: "secondary",
1042
1315
  asChild: true,
1043
1316
  children: /* @__PURE__ */ jsx(Link, {
@@ -1185,4 +1458,4 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
1185
1458
  }));
1186
1459
 
1187
1460
  //#endregion
1188
- export { AccountPlansPage, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
1461
+ export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -46,7 +46,7 @@
46
46
  "react": "19.2.4",
47
47
  "react-dom": "19.2.4",
48
48
  "tsdown": "0.20.3",
49
- "@lukoweb/apitogo": "0.1.65"
49
+ "@lukoweb/apitogo": "0.1.67"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": ">=19.2.0",