@burdenoff/microfe-devportal 2026.1112.2 → 2026.1113.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.
Files changed (31) hide show
  1. package/dist/devportal/components/AppCard.js +73 -70
  2. package/dist/devportal/components/AppCard.js.map +1 -1
  3. package/dist/devportal/components/ImageUploadField.js +82 -80
  4. package/dist/devportal/components/ImageUploadField.js.map +1 -1
  5. package/dist/devportal/components/StoreImage.d.ts +15 -0
  6. package/dist/devportal/components/StoreImage.js +30 -0
  7. package/dist/devportal/components/StoreImage.js.map +1 -0
  8. package/dist/devportal/components/index.d.ts +1 -0
  9. package/dist/devportal/components/index.js +17 -16
  10. package/dist/devportal/hooks/usePackageUpload.d.ts +46 -4
  11. package/dist/devportal/hooks/usePackageUpload.js +90 -36
  12. package/dist/devportal/hooks/usePackageUpload.js.map +1 -1
  13. package/dist/devportal/pages/AdminReviewPage.js +205 -202
  14. package/dist/devportal/pages/AdminReviewPage.js.map +1 -1
  15. package/dist/devportal/pages/ApplicationDetailPage.js +2 -2
  16. package/dist/devportal/pages/ApplicationDetailPage.js.map +1 -1
  17. package/dist/devportal/pages/ApplicationsPage.js +201 -199
  18. package/dist/devportal/pages/ApplicationsPage.js.map +1 -1
  19. package/dist/devportal/pages/CreatePhysicalItemPage.js +2 -2
  20. package/dist/devportal/pages/CreatePhysicalItemPage.js.map +1 -1
  21. package/dist/devportal/pages/EditPhysicalItemPage.js +312 -307
  22. package/dist/devportal/pages/EditPhysicalItemPage.js.map +1 -1
  23. package/dist/devportal/pages/ListingPreviewPage.js +175 -171
  24. package/dist/devportal/pages/ListingPreviewPage.js.map +1 -1
  25. package/dist/devportal/pages/SellerDashboardPage.js +118 -116
  26. package/dist/devportal/pages/SellerDashboardPage.js.map +1 -1
  27. package/dist/devportal/pages/SellerItemsPage.js +202 -199
  28. package/dist/devportal/pages/SellerItemsPage.js.map +1 -1
  29. package/dist/devportal/pages/StoreSubmissionPage.js +133 -131
  30. package/dist/devportal/pages/StoreSubmissionPage.js.map +1 -1
  31. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { useState as t } from "react";
3
+ import { ImageOff as n } from "lucide-react";
4
+ import { Fragment as r, jsx as i } from "react/jsx-runtime";
5
+ // src/devportal/components/StoreImage.tsx
6
+ var a = /^(?:https?:\/\/|data:image\/|blob:|\/(?!\/))/i;
7
+ function o(e) {
8
+ return typeof e == "string" && a.test(e.trim());
9
+ }
10
+ var s = ({ src: e, alt: n, fallback: a = null, ...s }) => {
11
+ let [c, l] = t(null);
12
+ return !o(e) || c === e ? /* @__PURE__ */ i(r, { children: a }) : /* @__PURE__ */ i("img", {
13
+ ...s,
14
+ src: e,
15
+ alt: n,
16
+ onError: () => l(e)
17
+ });
18
+ }, c = ({ className: t, iconSize: r = 18 }) => /* @__PURE__ */ i("div", {
19
+ role: "img",
20
+ "aria-label": "Image unavailable",
21
+ className: e("flex shrink-0 items-center justify-center rounded-input border border-border-subtle bg-bg-sunken", t),
22
+ children: /* @__PURE__ */ i(n, {
23
+ size: r,
24
+ className: "text-text-muted",
25
+ "aria-hidden": "true"
26
+ })
27
+ });
28
+ export { s as StoreImage, c as StoreImagePlaceholder, o as isRenderableImageSrc };
29
+
30
+ //# sourceMappingURL=StoreImage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StoreImage.js","names":["_Fragment","RENDERABLE_SRC","isRenderableImageSrc","src","StoreImage","imgProps","failedSrc","setFailedSrc","useState","Fragment","fallback","StoreImagePlaceholder","cn","className","ImageOff","iconSize"],"sources":["../../../src/devportal/components/StoreImage.tsx"],"sourcesContent":["import type { FC, ImgHTMLAttributes, ReactNode } from \"react\";\nimport { useState } from \"react\";\nimport { ImageOff } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\n\n// =============================================================================\n// STORE IMAGE\n//\n// Renders a store listing image (app/item icon, banner, screenshot) and swaps\n// in a fallback when the value can't be shown: empty, not a browser-loadable\n// URL (e.g. a bare Iconify name like \"mdi:robot\"), or the request fails (an\n// expired presigned link, a 404, a rate-limited third-party host).\n// fe-libs has no image-with-fallback primitive, so this lives here.\n// =============================================================================\n\nconst RENDERABLE_SRC = /^(?:https?:\\/\\/|data:image\\/|blob:|\\/(?!\\/))/i;\n\n/** True when `src` is a value the browser can load as an image. */\nexport function isRenderableImageSrc(\n src: string | null | undefined,\n): src is string {\n return typeof src === \"string\" && RENDERABLE_SRC.test(src.trim());\n}\n\nexport interface StoreImageProps extends Omit<\n ImgHTMLAttributes<HTMLImageElement>,\n \"src\" | \"onError\"\n> {\n src: string | null | undefined;\n alt: string;\n /** Shown instead of the image when it is missing or fails to load. */\n fallback?: ReactNode;\n}\n\nexport const StoreImage: FC<StoreImageProps> = ({\n src,\n alt,\n fallback = null,\n ...imgProps\n}) => {\n // Remember which src failed rather than a boolean, so a new src (e.g.\n // after a re-upload) gets a fresh attempt without an effect.\n const [failedSrc, setFailedSrc] = useState<string | null>(null);\n\n if (!isRenderableImageSrc(src) || failedSrc === src) {\n return <>{fallback}</>;\n }\n\n return (\n <img {...imgProps} src={src} alt={alt} onError={() => setFailedSrc(src)} />\n );\n};\n\n/** Neutral tile for a store image that can't be shown. Size it via className. */\nexport const StoreImagePlaceholder: FC<{\n className?: string;\n iconSize?: number;\n}> = ({ className, iconSize = 18 }) => (\n <div\n role=\"img\"\n aria-label=\"Image unavailable\"\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-input border border-border-subtle bg-bg-sunken\",\n className,\n )}\n >\n <ImageOff size={iconSize} className=\"text-text-muted\" aria-hidden=\"true\" />\n </div>\n);\n"],"mappings":";;;;;AAeA,IAAMC,IAAiB;AAGvB,SAAgBC,EACd,GACe;CACf,OAAO,OAAOC,KAAQ,YAAYF,EAAe,KAAKE,EAAI,KAAK,CAAC;AAClE;AAYA,IAAaC,KAAmC,EAC9C,QACA,QACA,cAAW,MACX,GAAGC,QACC;CAGJ,IAAM,CAACC,GAAWC,KAAgBC,EAAwB,IAAI;CAM9D,OAJI,CAACN,EAAqBC,CAAG,KAAKG,MAAcH,IACvC,kBAAAH,GAAA,EAAA,UAAGU,EAAW,CAAA,IAIrB,kBAAC,OAAD;EAAK,GAAIL;EAAe;EAAU;EAAK,eAAeE,EAAaJ,CAAG;CAAI,CAAA;AAE9E,GAGaQ,KAGP,EAAE,cAAW,cAAW,SAC5B,kBAAC,OAAD;CACE,MAAK;CACL,cAAW;CACX,WAAWC,EACT,oGACAC,CACF;CAEA,UAAA,kBAACC,GAAD;EAAU,MAAMC;EAAU,WAAU;EAAkB,eAAY;CAAQ,CAAA;AACvE,CAAA"}
@@ -9,6 +9,7 @@ export * from './PageLayout';
9
9
  export * from './StatCard';
10
10
  export * from './StatusBadge';
11
11
  export * from './AppCard';
12
+ export * from './StoreImage';
12
13
  export * from './SecretDisplay';
13
14
  export * from './DataTable';
14
15
  export * from './FormField';
@@ -4,19 +4,20 @@ import { PageLayout as r, PageSection as i } from "./PageLayout.js";
4
4
  import { StatCard as a, StatCardGrid as o } from "./StatCard.js";
5
5
  import { ActiveIndicator as s, FeaturedBadge as c, StatusBadge as l, VerifiedBadge as u } from "./StatusBadge.js";
6
6
  import { AppCard as d, AppListItem as f } from "./AppCard.js";
7
- import { NewSecretAlert as p, SecretDisplay as m } from "./SecretDisplay.js";
8
- import { DataTable as h, DetailList as g } from "./DataTable.js";
9
- import { CheckboxField as _, InputField as v, SelectField as y, TagInput as b, TextareaField as x } from "./FormField.js";
10
- import { Alert as S, ConfirmDialog as C } from "./ConfirmDialog.js";
11
- import { GrantDelegationModal as w } from "./GrantDelegationModal.js";
12
- import { ReportOrderIssueModal as T } from "./ReportOrderIssueModal.js";
13
- import { RegisterPickupLocationModal as E } from "./RegisterPickupLocationModal.js";
14
- import { PermissionGuard as D } from "./PermissionGuard.js";
15
- import { FeatureFlaggedRoute as O } from "./FeatureFlaggedRoute.js";
16
- import { TelemetryErrorBoundary as k } from "./TelemetryErrorBoundary.js";
17
- import { OrgSwitcher as A } from "./SectionNav.js";
18
- import { MarkdownEditor as j } from "./MarkdownEditor.js";
19
- import { ManifestViewer as M } from "./ManifestViewer.js";
20
- import { PackageValidationPanel as N, readPackageValidationState as P } from "./PackageValidationPanel.js";
21
- import { NodeSmokePanel as F, readNodeSmokeState as I } from "./NodeSmokePanel.js";
22
- export { s as ActiveIndicator, S as Alert, d as AppCard, f as AppListItem, _ as CheckboxField, C as ConfirmDialog, h as DataTable, g as DetailList, n as EmptyState, O as FeatureFlaggedRoute, c as FeaturedBadge, w as GrantDelegationModal, v as InputField, e as LoadingState, M as ManifestViewer, j as MarkdownEditor, p as NewSecretAlert, F as NodeSmokePanel, A as OrgSwitcher, N as PackageValidationPanel, r as PageLayout, t as PageLoadingState, i as PageSection, D as PermissionGuard, E as RegisterPickupLocationModal, T as ReportOrderIssueModal, m as SecretDisplay, y as SelectField, a as StatCard, o as StatCardGrid, l as StatusBadge, b as TagInput, k as TelemetryErrorBoundary, x as TextareaField, u as VerifiedBadge, I as readNodeSmokeState, P as readPackageValidationState };
7
+ import { StoreImage as p, StoreImagePlaceholder as m, isRenderableImageSrc as h } from "./StoreImage.js";
8
+ import { NewSecretAlert as g, SecretDisplay as _ } from "./SecretDisplay.js";
9
+ import { DataTable as v, DetailList as y } from "./DataTable.js";
10
+ import { CheckboxField as b, InputField as x, SelectField as S, TagInput as C, TextareaField as w } from "./FormField.js";
11
+ import { Alert as T, ConfirmDialog as E } from "./ConfirmDialog.js";
12
+ import { GrantDelegationModal as D } from "./GrantDelegationModal.js";
13
+ import { ReportOrderIssueModal as O } from "./ReportOrderIssueModal.js";
14
+ import { RegisterPickupLocationModal as k } from "./RegisterPickupLocationModal.js";
15
+ import { PermissionGuard as A } from "./PermissionGuard.js";
16
+ import { FeatureFlaggedRoute as j } from "./FeatureFlaggedRoute.js";
17
+ import { TelemetryErrorBoundary as M } from "./TelemetryErrorBoundary.js";
18
+ import { OrgSwitcher as N } from "./SectionNav.js";
19
+ import { MarkdownEditor as P } from "./MarkdownEditor.js";
20
+ import { ManifestViewer as F } from "./ManifestViewer.js";
21
+ import { PackageValidationPanel as I, readPackageValidationState as L } from "./PackageValidationPanel.js";
22
+ import { NodeSmokePanel as R, readNodeSmokeState as z } from "./NodeSmokePanel.js";
23
+ export { s as ActiveIndicator, T as Alert, d as AppCard, f as AppListItem, b as CheckboxField, E as ConfirmDialog, v as DataTable, y as DetailList, n as EmptyState, j as FeatureFlaggedRoute, c as FeaturedBadge, D as GrantDelegationModal, x as InputField, e as LoadingState, F as ManifestViewer, P as MarkdownEditor, g as NewSecretAlert, R as NodeSmokePanel, N as OrgSwitcher, I as PackageValidationPanel, r as PageLayout, t as PageLoadingState, i as PageSection, A as PermissionGuard, k as RegisterPickupLocationModal, O as ReportOrderIssueModal, _ as SecretDisplay, S as SelectField, a as StatCard, o as StatCardGrid, l as StatusBadge, p as StoreImage, m as StoreImagePlaceholder, C as TagInput, M as TelemetryErrorBoundary, w as TextareaField, u as VerifiedBadge, h as isRenderableImageSrc, z as readNodeSmokeState, L as readPackageValidationState };
@@ -1,10 +1,27 @@
1
1
  export interface PackageUploadResult {
2
2
  fileId: string;
3
- /** The permanent publicUrl when available, else the presigned downloadUrl
4
- * (field name kept as `downloadUrl` so existing callers don't need to
5
- * change — see the module doc comment above). */
3
+ /** The permanent publicUrl when available, else (package uploads only) the
4
+ * presigned downloadUrl. Field name kept as `downloadUrl` so existing
5
+ * callers don't need to change — see the module doc comment above. */
6
6
  downloadUrl: string;
7
7
  }
8
+ export interface PackageUploadOptions {
9
+ /** Fail instead of falling back to the presigned downloadUrl when the
10
+ * file has no permanent publicUrl. Set for anything rendered later as an
11
+ * image (icons, banners, screenshots). */
12
+ requirePublicUrl?: boolean;
13
+ /** Delays between publicUrl re-reads when `requirePublicUrl` is set. */
14
+ publicUrlRetryDelaysMs?: readonly number[];
15
+ }
16
+ /** Folder for store listing images (icons, banners, screenshots). */
17
+ export declare const ASSET_UPLOAD_FOLDER = "/DevPortal/Assets";
18
+ /** Default folder for package (zip) uploads. */
19
+ export declare const PACKAGE_UPLOAD_FOLDER = "/DevPortal/Packages";
20
+ /** Backoff used to re-read a file's publicUrl after completeUpload. */
21
+ export declare const DEFAULT_PUBLIC_URL_RETRY_DELAYS_MS: readonly number[];
22
+ /** Shown when an image upload succeeded but has no permanent link yet. */
23
+ export declare const PUBLIC_URL_NOT_READY_MESSAGE = "The image was uploaded but its public link is not ready yet. Please try again.";
24
+ type Sleep = (ms: number) => Promise<void>;
8
25
  export interface PackageUploadState {
9
26
  uploading: boolean;
10
27
  progress: number;
@@ -12,7 +29,32 @@ export interface PackageUploadState {
12
29
  }
13
30
  type GqlFetcher = (query: string, variables: Record<string, unknown>) => Promise<Record<string, unknown>>;
14
31
  export declare function makeFetcher(workspacesGatewayUrl: string, authToken: string | undefined, workspaceId: string | undefined, workspaceToken: string | undefined): GqlFetcher;
15
- export declare function usePackageUpload(workspacesGatewayUrl: string | undefined, authToken: string | undefined, workspaceId: string | undefined, getWorkspaceToken: (() => Promise<string | null>) | undefined, folderPath?: string): {
32
+ interface UploadedFile {
33
+ id: string;
34
+ downloadUrl?: string;
35
+ publicUrl?: string | null;
36
+ }
37
+ export declare function completeUpload(gql: GqlFetcher, sessionId: string, folderId: string, fileSize: number): Promise<UploadedFile>;
38
+ /** Re-reads the file's permanent publicUrl (null while the mirror is pending). */
39
+ export declare function fetchPublicUrl(gql: GqlFetcher, fileId: string): Promise<string | undefined>;
40
+ /**
41
+ * Picks the URL to persist for a completed upload. Package uploads fall back
42
+ * to the presigned downloadUrl; asset uploads (`requirePublicUrl`) retry the
43
+ * publicUrl and throw PUBLIC_URL_NOT_READY_MESSAGE rather than return a
44
+ * presigned URL that expires.
45
+ */
46
+ export declare function resolvePersistableUrl(gql: GqlFetcher, file: UploadedFile, options?: PackageUploadOptions, sleep?: Sleep): Promise<PackageUploadResult>;
47
+ export declare function usePackageUpload(workspacesGatewayUrl: string | undefined, authToken: string | undefined, workspaceId: string | undefined, getWorkspaceToken: (() => Promise<string | null>) | undefined, folderPath?: string, options?: PackageUploadOptions): {
48
+ uploading: boolean;
49
+ progress: number;
50
+ error: string | null;
51
+ upload: (file: File) => Promise<PackageUploadResult>;
52
+ };
53
+ /**
54
+ * Upload hook for store listing images (icons, banners, screenshots). Stores
55
+ * under ASSET_UPLOAD_FOLDER and only ever returns the permanent publicUrl.
56
+ */
57
+ export declare function useAssetUpload(workspacesGatewayUrl: string | undefined, authToken: string | undefined, workspaceId: string | undefined, getWorkspaceToken: (() => Promise<string | null>) | undefined): {
16
58
  uploading: boolean;
17
59
  progress: number;
18
60
  error: string | null;
@@ -1,26 +1,32 @@
1
1
  import { graphQLErrorFrom as e, httpErrorFrom as t } from "../utils/uploadErrors.js";
2
- import { useCallback as n, useState as r } from "react";
2
+ import { useCallback as n, useMemo as r, useState as i } from "react";
3
3
  // src/devportal/hooks/usePackageUpload.ts
4
- async function i(e) {
4
+ var a = "/DevPortal/Assets", o = "/DevPortal/Packages", s = [
5
+ 500,
6
+ 1e3,
7
+ 2e3,
8
+ 4e3
9
+ ], c = "The image was uploaded but its public link is not ready yet. Please try again.", l = (e) => new Promise((t) => setTimeout(t, e));
10
+ async function u(e) {
5
11
  try {
6
12
  return await e.json();
7
13
  } catch {
8
14
  return;
9
15
  }
10
16
  }
11
- function a(n, r, a, o) {
12
- return async (s, c) => {
13
- let l = { "Content-Type": "application/json" };
14
- r && (l.Authorization = `Bearer ${r}`), o && (l["X-Workspace-Authorization"] = `Bearer ${o}`), a && (l["x-workspace-id"] = a);
15
- let u = n.endsWith("/graphql") ? n : `${n}/workspaces/graphql`, d = await fetch(u, {
17
+ function d(n, r, i, a) {
18
+ return async (o, s) => {
19
+ let c = { "Content-Type": "application/json" };
20
+ r && (c.Authorization = `Bearer ${r}`), a && (c["X-Workspace-Authorization"] = `Bearer ${a}`), i && (c["x-workspace-id"] = i);
21
+ let l = n.endsWith("/graphql") ? n : `${n}/workspaces/graphql`, d = await fetch(l, {
16
22
  method: "POST",
17
- headers: l,
23
+ headers: c,
18
24
  credentials: "include",
19
25
  body: JSON.stringify({
20
- query: s,
21
- variables: c
26
+ query: o,
27
+ variables: s
22
28
  })
23
- }), f = await i(d);
29
+ }), f = await u(d);
24
30
  if (!d.ok) throw t(d.status, d.statusText, f);
25
31
  let p = e(f);
26
32
  if (p) throw p;
@@ -28,12 +34,12 @@ function a(n, r, a, o) {
28
34
  return typeof m == "object" && m ? m : {};
29
35
  };
30
36
  }
31
- async function o(e, t) {
37
+ async function f(e, t) {
32
38
  let n = (await e("mutation DevPortal_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) { id }\n }", { path: t })).ensureFolderByPath?.id;
33
39
  if (!n) throw Error("ensureFolderByPath returned no id");
34
40
  return n;
35
41
  }
36
- async function s(e, t, n) {
42
+ async function p(e, t, n) {
37
43
  let r = t.name.includes(".") ? t.name.substring(t.name.lastIndexOf(".") + 1) : "zip", i = (await e("mutation DevPortal_InitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }", { input: {
38
44
  fileName: t.name,
39
45
  fileSize: String(t.size),
@@ -48,7 +54,7 @@ async function s(e, t, n) {
48
54
  uploadUrl: o
49
55
  };
50
56
  }
51
- async function c(e, t, n) {
57
+ async function m(e, t, n) {
52
58
  await new Promise((r, i) => {
53
59
  let a = new XMLHttpRequest();
54
60
  a.upload.addEventListener("progress", (e) => {
@@ -58,20 +64,58 @@ async function c(e, t, n) {
58
64
  }), a.addEventListener("error", () => i(/* @__PURE__ */ Error("S3 PUT network error"))), a.addEventListener("abort", () => i(/* @__PURE__ */ Error("S3 PUT aborted"))), a.open("PUT", t), a.setRequestHeader("Content-Type", e.type || "application/zip"), a.send(e);
59
65
  });
60
66
  }
61
- async function l(e, t, n, r) {
67
+ function h(e) {
68
+ return typeof e == "string" && e.length > 0 ? e : void 0;
69
+ }
70
+ async function g(e, t, n, r) {
62
71
  let i = (await e("mutation DevPortal_CompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n file { id downloadUrl publicUrl }\n }\n }", { input: {
63
72
  sessionId: t,
64
73
  folderId: n,
65
74
  fileSize: String(r)
66
- } })).completeUpload?.file, a = i?.publicUrl || i?.downloadUrl;
67
- if (!i?.id || !a) throw Error("completeUpload did not return file id or a usable URL");
75
+ } })).completeUpload?.file, a = h(i?.id);
76
+ if (!a) throw Error("completeUpload did not return a file id");
68
77
  return {
69
- fileId: i.id,
70
- downloadUrl: a
78
+ id: a,
79
+ downloadUrl: i?.downloadUrl,
80
+ publicUrl: i?.publicUrl
71
81
  };
72
82
  }
73
- function u(e, t, i, u, d = "/DevPortal/Packages") {
74
- let [f, p] = r({
83
+ async function _(e, t) {
84
+ let n = (await e("query DevPortal_FilePublicUrl($id: ID!) {\n getFile(id: $id) { id publicUrl }\n }", { id: t })).getFile;
85
+ return h(n?.publicUrl);
86
+ }
87
+ async function v(e, t, n = {}, r = l) {
88
+ let i = h(t.publicUrl);
89
+ if (i) return {
90
+ fileId: t.id,
91
+ downloadUrl: i
92
+ };
93
+ if (!n.requirePublicUrl) {
94
+ let e = h(t.downloadUrl);
95
+ if (!e) throw Error("completeUpload did not return a usable URL");
96
+ return {
97
+ fileId: t.id,
98
+ downloadUrl: e
99
+ };
100
+ }
101
+ let a = n.publicUrlRetryDelaysMs ?? s;
102
+ for (let n of a) {
103
+ await r(n);
104
+ let i;
105
+ try {
106
+ i = await _(e, t.id);
107
+ } catch {
108
+ i = void 0;
109
+ }
110
+ if (i) return {
111
+ fileId: t.id,
112
+ downloadUrl: i
113
+ };
114
+ }
115
+ throw Error(c);
116
+ }
117
+ function y(e, t, a, s, c = o, l = {}) {
118
+ let { requirePublicUrl: u = !1, publicUrlRetryDelaysMs: h } = l, _ = h?.join(","), y = r(() => _ === void 0 ? void 0 : _.split(",").filter((e) => e !== "").map(Number), [_]), [b, x] = i({
75
119
  uploading: !1,
76
120
  progress: 0,
77
121
  error: null
@@ -79,38 +123,41 @@ function u(e, t, i, u, d = "/DevPortal/Packages") {
79
123
  return {
80
124
  upload: n(async (n) => {
81
125
  if (!e) throw Error("workspacesGatewayUrl is not configured. Pass it as a prop to DevportalRoot.");
82
- let r = a(e, t, i, (u ? await u() : void 0) ?? void 0);
83
- p({
126
+ let r = d(e, t, a, (s ? await s() : void 0) ?? void 0);
127
+ x({
84
128
  uploading: !0,
85
129
  progress: 0,
86
130
  error: null
87
131
  });
88
132
  try {
89
- let e = await o(r, d);
90
- p((e) => ({
133
+ let e = await f(r, c);
134
+ x((e) => ({
91
135
  ...e,
92
136
  progress: 5
93
137
  }));
94
- let { sessionId: t, uploadUrl: i } = await s(r, n, e);
95
- p((e) => ({
138
+ let { sessionId: t, uploadUrl: i } = await p(r, n, e);
139
+ x((e) => ({
96
140
  ...e,
97
141
  progress: 10
98
- })), await c(n, i, (e) => p((t) => ({
142
+ })), await m(n, i, (e) => x((t) => ({
99
143
  ...t,
100
144
  progress: e
101
- }))), p((e) => ({
145
+ }))), x((e) => ({
102
146
  ...e,
103
147
  progress: 90
104
148
  }));
105
- let a = await l(r, t, e, n.size);
106
- return p({
149
+ let a = await v(r, await g(r, t, e, n.size), {
150
+ requirePublicUrl: u,
151
+ publicUrlRetryDelaysMs: y
152
+ });
153
+ return x({
107
154
  uploading: !1,
108
155
  progress: 100,
109
156
  error: null
110
157
  }), a;
111
158
  } catch (e) {
112
159
  let t = e instanceof Error ? e.message : "Upload failed";
113
- throw p({
160
+ throw x({
114
161
  uploading: !1,
115
162
  progress: 0,
116
163
  error: t
@@ -119,12 +166,19 @@ function u(e, t, i, u, d = "/DevPortal/Packages") {
119
166
  }, [
120
167
  e,
121
168
  t,
122
- i,
123
- u
169
+ a,
170
+ s,
171
+ c,
172
+ u,
173
+ y
124
174
  ]),
125
- ...f
175
+ ...b
126
176
  };
127
177
  }
128
- export { a as makeFetcher, u as usePackageUpload };
178
+ var b = { requirePublicUrl: !0 };
179
+ function x(e, t, n, r) {
180
+ return y(e, t, n, r, a, b);
181
+ }
182
+ export { a as ASSET_UPLOAD_FOLDER, s as DEFAULT_PUBLIC_URL_RETRY_DELAYS_MS, o as PACKAGE_UPLOAD_FOLDER, c as PUBLIC_URL_NOT_READY_MESSAGE, g as completeUpload, _ as fetchPublicUrl, d as makeFetcher, v as resolvePersistableUrl, x as useAssetUpload, y as usePackageUpload };
129
183
 
130
184
  //# sourceMappingURL=usePackageUpload.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePackageUpload.js","names":["readJsonBody","res","makeFetcher","headers","authToken","\"Authorization\"","workspaceToken","workspaceId","endpoint","workspacesGatewayUrl","body","httpErrorFrom","inBand","graphQLErrorFrom","data","ensureFolder","id","gql","initiateUpload","ext","file","result","sessionId","uploadUrl","putToPresignedUrl","xhr","evt","onProgress","resolve","reject","completeUpload","fileSize","url","usePackageUpload","state","setState","useState","useCallback","getWorkspaceToken","folderId","folderPath","s","pct","err","message"],"sources":["../../../src/devportal/hooks/usePackageUpload.ts"],"sourcesContent":["/**\n * usePackageUpload — uploads a file to wspace-files-svc and returns a URL to\n * persist (ApplicationVersion.packageUrl, a physical item's image, an app\n * icon — this hook is generic despite its name; folderPath picks the\n * destination). Every upload here is visibility: PUBLIC (see\n * initiateUpload below), so the returned URL prefers the permanent CDN\n * publicUrl over the presigned downloadUrl (~1 hour TTL, wrong for anything\n * persisted and displayed later — see wspace-files-svc's FieldResolvers.ts\n * doc comment on `downloadUrl`). Falls back to downloadUrl when publicUrl\n * is null (the public-assets CDN infra isn't provisioned in every\n * environment yet), so this keeps working exactly as before until it is.\n *\n * Flow (mirrors uploadBlobToFiles in @burdenoff/fe-libs/storage):\n * 1. ensureFolderByPath(folderPath) → folderId\n * 2. initiateUpload → { sessionId, presignedUrl }\n * 3. PUT blob to presignedUrl (progress tracked via XHR)\n * 4. completeUpload → { file.publicUrl ?? file.downloadUrl }\n *\n * Uses raw fetch for GraphQL mutations so we can target the workspace gateway\n * without a second ApolloProvider.\n */\nimport { useCallback, useState } from \"react\";\nimport { graphQLErrorFrom, httpErrorFrom } from \"../utils/uploadErrors\";\n\nexport interface PackageUploadResult {\n fileId: string;\n /** The permanent publicUrl when available, else the presigned downloadUrl\n * (field name kept as `downloadUrl` so existing callers don't need to\n * change — see the module doc comment above). */\n downloadUrl: string;\n}\n\nexport interface PackageUploadState {\n uploading: boolean;\n progress: number;\n error: string | null;\n}\n\ntype GqlFetcher = (\n query: string,\n variables: Record<string, unknown>,\n) => Promise<Record<string, unknown>>;\n\n/** The parsed JSON body, or undefined when it is missing or not JSON. */\nasync function readJsonBody(res: Response): Promise<unknown> {\n try {\n return await res.json();\n } catch {\n return undefined;\n }\n}\n\nexport function makeFetcher(\n workspacesGatewayUrl: string,\n authToken: string | undefined,\n workspaceId: string | undefined,\n workspaceToken: string | undefined,\n): GqlFetcher {\n return async (query, variables) => {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n if (authToken) headers[\"Authorization\"] = `Bearer ${authToken}`;\n if (workspaceToken)\n headers[\"X-Workspace-Authorization\"] = `Bearer ${workspaceToken}`;\n if (workspaceId) headers[\"x-workspace-id\"] = workspaceId;\n\n const endpoint = workspacesGatewayUrl.endsWith(\"/graphql\")\n ? workspacesGatewayUrl\n : `${workspacesGatewayUrl}/workspaces/graphql`;\n\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers,\n credentials: \"include\",\n body: JSON.stringify({ query, variables }),\n });\n // Read the body even on a non-2xx status: the gateway explains a quota\n // refusal (HTTP 429) in the GraphQL errors, and the status alone reads as\n // rate limiting (BOFF-10676).\n const body = await readJsonBody(res);\n if (!res.ok) {\n throw httpErrorFrom(res.status, res.statusText, body);\n }\n const inBand = graphQLErrorFrom(body);\n if (inBand) throw inBand;\n // Cast: `body` is untrusted JSON; only `data` is read, and its shape is checked below.\n const data = (body as { data?: unknown } | undefined)?.data;\n return typeof data === \"object\" && data !== null\n ? // A GraphQL `data` member is always an object keyed by root field.\n (data as Record<string, unknown>)\n : {};\n };\n}\n\nasync function ensureFolder(gql: GqlFetcher, path: string): Promise<string> {\n const data = await gql(\n `mutation DevPortal_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) { id }\n }`,\n { path },\n );\n const id = (data.ensureFolderByPath as { id?: string } | undefined)?.id;\n if (!id) throw new Error(\"ensureFolderByPath returned no id\");\n return id;\n}\n\nasync function initiateUpload(\n gql: GqlFetcher,\n file: File,\n folderId: string,\n): Promise<{ sessionId: string; uploadUrl: string }> {\n const ext = file.name.includes(\".\")\n ? file.name.substring(file.name.lastIndexOf(\".\") + 1)\n : \"zip\";\n const data = await gql(\n `mutation DevPortal_InitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }`,\n {\n input: {\n fileName: file.name,\n fileSize: String(file.size),\n mimeType: file.type || \"application/zip\",\n extension: ext,\n folderId,\n visibility: \"PUBLIC\",\n },\n },\n );\n const result = data.initiateUpload as\n { uploadUrl?: string; session?: { id?: string } } | undefined;\n const sessionId = result?.session?.id;\n const uploadUrl = result?.uploadUrl;\n if (!sessionId || !uploadUrl) {\n throw new Error(\"initiateUpload did not return sessionId or uploadUrl\");\n }\n return { sessionId, uploadUrl };\n}\n\nasync function putToPresignedUrl(\n file: File,\n uploadUrl: string,\n onProgress: (pct: number) => void,\n): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.upload.addEventListener(\"progress\", (evt) => {\n if (evt.lengthComputable) {\n onProgress(10 + Math.round((evt.loaded / evt.total) * 80));\n }\n });\n xhr.addEventListener(\"load\", () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n reject(new Error(`S3 PUT failed: ${xhr.status}`));\n }\n });\n xhr.addEventListener(\"error\", () =>\n reject(new Error(\"S3 PUT network error\")),\n );\n xhr.addEventListener(\"abort\", () => reject(new Error(\"S3 PUT aborted\")));\n xhr.open(\"PUT\", uploadUrl);\n xhr.setRequestHeader(\"Content-Type\", file.type || \"application/zip\");\n xhr.send(file);\n });\n}\n\nasync function completeUpload(\n gql: GqlFetcher,\n sessionId: string,\n folderId: string,\n fileSize: number,\n): Promise<PackageUploadResult> {\n const data = await gql(\n `mutation DevPortal_CompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n file { id downloadUrl publicUrl }\n }\n }`,\n {\n input: {\n sessionId,\n folderId,\n fileSize: String(fileSize),\n },\n },\n );\n const file = (\n data.completeUpload as\n | {\n file?: {\n id?: string;\n downloadUrl?: string;\n publicUrl?: string | null;\n };\n }\n | undefined\n )?.file;\n const url = file?.publicUrl || file?.downloadUrl;\n if (!file?.id || !url) {\n throw new Error(\"completeUpload did not return file id or a usable URL\");\n }\n return { fileId: file.id, downloadUrl: url };\n}\n\nexport function usePackageUpload(\n workspacesGatewayUrl: string | undefined,\n authToken: string | undefined,\n workspaceId: string | undefined,\n getWorkspaceToken: (() => Promise<string | null>) | undefined,\n folderPath = \"/DevPortal/Packages\",\n) {\n const [state, setState] = useState<PackageUploadState>({\n uploading: false,\n progress: 0,\n error: null,\n });\n\n const upload = useCallback(\n async (file: File): Promise<PackageUploadResult> => {\n if (!workspacesGatewayUrl) {\n throw new Error(\n \"workspacesGatewayUrl is not configured. Pass it as a prop to DevportalRoot.\",\n );\n }\n\n const freshToken = getWorkspaceToken\n ? await getWorkspaceToken()\n : undefined;\n const gql = makeFetcher(\n workspacesGatewayUrl,\n authToken,\n workspaceId,\n freshToken ?? undefined,\n );\n\n setState({ uploading: true, progress: 0, error: null });\n try {\n const folderId = await ensureFolder(gql, folderPath);\n setState((s) => ({ ...s, progress: 5 }));\n\n const { sessionId, uploadUrl } = await initiateUpload(\n gql,\n file,\n folderId,\n );\n setState((s) => ({ ...s, progress: 10 }));\n\n await putToPresignedUrl(file, uploadUrl, (pct) =>\n setState((s) => ({ ...s, progress: pct })),\n );\n setState((s) => ({ ...s, progress: 90 }));\n\n const result = await completeUpload(\n gql,\n sessionId,\n folderId,\n file.size,\n );\n setState({ uploading: false, progress: 100, error: null });\n return result;\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Upload failed\";\n setState({ uploading: false, progress: 0, error: message });\n throw err;\n }\n },\n [workspacesGatewayUrl, authToken, workspaceId, getWorkspaceToken],\n );\n\n return { upload, ...state };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"usePackageUpload.js","names":["ASSET_UPLOAD_FOLDER","PACKAGE_UPLOAD_FOLDER","DEFAULT_PUBLIC_URL_RETRY_DELAYS_MS","PUBLIC_URL_NOT_READY_MESSAGE","defaultSleep","resolve","ms","readJsonBody","res","makeFetcher","headers","authToken","\"Authorization\"","workspaceToken","workspaceId","endpoint","workspacesGatewayUrl","body","httpErrorFrom","inBand","graphQLErrorFrom","data","ensureFolder","id","gql","initiateUpload","ext","file","result","sessionId","uploadUrl","putToPresignedUrl","xhr","evt","onProgress","reject","nonEmpty","value","completeUpload","fileSize","fetchPublicUrl","fileId","resolvePersistableUrl","publicUrl","options","downloadUrl","delays","delay","sleep","retried","usePackageUpload","retryDelaysKey","publicUrlRetryDelaysMs","retryDelays","useMemo","part","state","setState","useState","useCallback","getWorkspaceToken","folderId","folderPath","s","pct","err","message","requirePublicUrl","ASSET_UPLOAD_OPTIONS","useAssetUpload"],"sources":["../../../src/devportal/hooks/usePackageUpload.ts"],"sourcesContent":["/**\n * usePackageUpload — uploads a file to wspace-files-svc and returns a URL to\n * persist (ApplicationVersion.packageUrl, a physical item's image, an app\n * icon — this hook is generic despite its name; folderPath picks the\n * destination). Every upload here is visibility: PUBLIC (see\n * initiateUpload below), so the returned URL prefers the permanent CDN\n * publicUrl over the presigned downloadUrl (~1 hour TTL, wrong for anything\n * persisted and displayed later — see wspace-files-svc's FieldResolvers.ts\n * doc comment on `downloadUrl`).\n *\n * Two kinds of upload go through this hook:\n * - Packages (the default, `/DevPortal/Packages`): when publicUrl is null\n * the presigned downloadUrl is still returned. global-devportal-svc\n * re-signs package URLs from their S3 key, so a short-lived URL is fine.\n * - Display assets (icons, banners, screenshots — use `useAssetUpload`,\n * which sets `requirePublicUrl`): a presigned URL must never be\n * persisted, because nothing re-signs it and it breaks an hour later in\n * the store. The files-svc public mirror is best-effort, so when\n * completeUpload returns no publicUrl the file is re-read a few times\n * with a short backoff; if publicUrl is still null the upload fails with\n * a user-visible error instead of returning the presigned URL.\n *\n * Flow (mirrors uploadBlobToFiles in @burdenoff/fe-libs/storage):\n * 1. ensureFolderByPath(folderPath) → folderId\n * 2. initiateUpload → { sessionId, presignedUrl }\n * 3. PUT blob to presignedUrl (progress tracked via XHR)\n * 4. completeUpload → { file.publicUrl ?? file.downloadUrl }\n * 5. (assets only) getFile(id).publicUrl retries when step 4 had none\n *\n * Uses raw fetch for GraphQL mutations so we can target the workspace gateway\n * without a second ApolloProvider.\n */\nimport { useCallback, useMemo, useState } from \"react\";\nimport { graphQLErrorFrom, httpErrorFrom } from \"../utils/uploadErrors\";\n\nexport interface PackageUploadResult {\n fileId: string;\n /** The permanent publicUrl when available, else (package uploads only) the\n * presigned downloadUrl. Field name kept as `downloadUrl` so existing\n * callers don't need to change — see the module doc comment above. */\n downloadUrl: string;\n}\n\nexport interface PackageUploadOptions {\n /** Fail instead of falling back to the presigned downloadUrl when the\n * file has no permanent publicUrl. Set for anything rendered later as an\n * image (icons, banners, screenshots). */\n requirePublicUrl?: boolean;\n /** Delays between publicUrl re-reads when `requirePublicUrl` is set. */\n publicUrlRetryDelaysMs?: readonly number[];\n}\n\n/** Folder for store listing images (icons, banners, screenshots). */\nexport const ASSET_UPLOAD_FOLDER = \"/DevPortal/Assets\";\n\n/** Default folder for package (zip) uploads. */\nexport const PACKAGE_UPLOAD_FOLDER = \"/DevPortal/Packages\";\n\n/** Backoff used to re-read a file's publicUrl after completeUpload. */\nexport const DEFAULT_PUBLIC_URL_RETRY_DELAYS_MS: readonly number[] = [\n 500, 1000, 2000, 4000,\n];\n\n/** Shown when an image upload succeeded but has no permanent link yet. */\nexport const PUBLIC_URL_NOT_READY_MESSAGE =\n \"The image was uploaded but its public link is not ready yet. Please try again.\";\n\ntype Sleep = (ms: number) => Promise<void>;\n\nconst defaultSleep: Sleep = (ms) =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\nexport interface PackageUploadState {\n uploading: boolean;\n progress: number;\n error: string | null;\n}\n\ntype GqlFetcher = (\n query: string,\n variables: Record<string, unknown>,\n) => Promise<Record<string, unknown>>;\n\n/** The parsed JSON body, or undefined when it is missing or not JSON. */\nasync function readJsonBody(res: Response): Promise<unknown> {\n try {\n return await res.json();\n } catch {\n return undefined;\n }\n}\n\nexport function makeFetcher(\n workspacesGatewayUrl: string,\n authToken: string | undefined,\n workspaceId: string | undefined,\n workspaceToken: string | undefined,\n): GqlFetcher {\n return async (query, variables) => {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n if (authToken) headers[\"Authorization\"] = `Bearer ${authToken}`;\n if (workspaceToken)\n headers[\"X-Workspace-Authorization\"] = `Bearer ${workspaceToken}`;\n if (workspaceId) headers[\"x-workspace-id\"] = workspaceId;\n\n const endpoint = workspacesGatewayUrl.endsWith(\"/graphql\")\n ? workspacesGatewayUrl\n : `${workspacesGatewayUrl}/workspaces/graphql`;\n\n const res = await fetch(endpoint, {\n method: \"POST\",\n headers,\n credentials: \"include\",\n body: JSON.stringify({ query, variables }),\n });\n // Read the body even on a non-2xx status: the gateway explains a quota\n // refusal (HTTP 429) in the GraphQL errors, and the status alone reads as\n // rate limiting (BOFF-10676).\n const body = await readJsonBody(res);\n if (!res.ok) {\n throw httpErrorFrom(res.status, res.statusText, body);\n }\n const inBand = graphQLErrorFrom(body);\n if (inBand) throw inBand;\n // Cast: `body` is untrusted JSON; only `data` is read, and its shape is checked below.\n const data = (body as { data?: unknown } | undefined)?.data;\n return typeof data === \"object\" && data !== null\n ? // A GraphQL `data` member is always an object keyed by root field.\n (data as Record<string, unknown>)\n : {};\n };\n}\n\nasync function ensureFolder(gql: GqlFetcher, path: string): Promise<string> {\n const data = await gql(\n `mutation DevPortal_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) { id }\n }`,\n { path },\n );\n const id = (data.ensureFolderByPath as { id?: string } | undefined)?.id;\n if (!id) throw new Error(\"ensureFolderByPath returned no id\");\n return id;\n}\n\nasync function initiateUpload(\n gql: GqlFetcher,\n file: File,\n folderId: string,\n): Promise<{ sessionId: string; uploadUrl: string }> {\n const ext = file.name.includes(\".\")\n ? file.name.substring(file.name.lastIndexOf(\".\") + 1)\n : \"zip\";\n const data = await gql(\n `mutation DevPortal_InitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session { id }\n }\n }`,\n {\n input: {\n fileName: file.name,\n fileSize: String(file.size),\n mimeType: file.type || \"application/zip\",\n extension: ext,\n folderId,\n visibility: \"PUBLIC\",\n },\n },\n );\n const result = data.initiateUpload as\n { uploadUrl?: string; session?: { id?: string } } | undefined;\n const sessionId = result?.session?.id;\n const uploadUrl = result?.uploadUrl;\n if (!sessionId || !uploadUrl) {\n throw new Error(\"initiateUpload did not return sessionId or uploadUrl\");\n }\n return { sessionId, uploadUrl };\n}\n\nasync function putToPresignedUrl(\n file: File,\n uploadUrl: string,\n onProgress: (pct: number) => void,\n): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.upload.addEventListener(\"progress\", (evt) => {\n if (evt.lengthComputable) {\n onProgress(10 + Math.round((evt.loaded / evt.total) * 80));\n }\n });\n xhr.addEventListener(\"load\", () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve();\n } else {\n reject(new Error(`S3 PUT failed: ${xhr.status}`));\n }\n });\n xhr.addEventListener(\"error\", () =>\n reject(new Error(\"S3 PUT network error\")),\n );\n xhr.addEventListener(\"abort\", () => reject(new Error(\"S3 PUT aborted\")));\n xhr.open(\"PUT\", uploadUrl);\n xhr.setRequestHeader(\"Content-Type\", file.type || \"application/zip\");\n xhr.send(file);\n });\n}\n\ninterface UploadedFile {\n id: string;\n downloadUrl?: string;\n publicUrl?: string | null;\n}\n\n/** A non-empty string, else undefined. */\nfunction nonEmpty(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nexport async function completeUpload(\n gql: GqlFetcher,\n sessionId: string,\n folderId: string,\n fileSize: number,\n): Promise<UploadedFile> {\n const data = await gql(\n `mutation DevPortal_CompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n file { id downloadUrl publicUrl }\n }\n }`,\n {\n input: {\n sessionId,\n folderId,\n fileSize: String(fileSize),\n },\n },\n );\n const file = (\n data.completeUpload as\n | {\n file?: {\n id?: string;\n downloadUrl?: string;\n publicUrl?: string | null;\n };\n }\n | undefined\n )?.file;\n const id = nonEmpty(file?.id);\n if (!id) {\n throw new Error(\"completeUpload did not return a file id\");\n }\n return { id, downloadUrl: file?.downloadUrl, publicUrl: file?.publicUrl };\n}\n\n/** Re-reads the file's permanent publicUrl (null while the mirror is pending). */\nexport async function fetchPublicUrl(\n gql: GqlFetcher,\n fileId: string,\n): Promise<string | undefined> {\n const data = await gql(\n `query DevPortal_FilePublicUrl($id: ID!) {\n getFile(id: $id) { id publicUrl }\n }`,\n { id: fileId },\n );\n const file = data.getFile as { publicUrl?: string | null } | undefined;\n return nonEmpty(file?.publicUrl);\n}\n\n/**\n * Picks the URL to persist for a completed upload. Package uploads fall back\n * to the presigned downloadUrl; asset uploads (`requirePublicUrl`) retry the\n * publicUrl and throw PUBLIC_URL_NOT_READY_MESSAGE rather than return a\n * presigned URL that expires.\n */\nexport async function resolvePersistableUrl(\n gql: GqlFetcher,\n file: UploadedFile,\n options: PackageUploadOptions = {},\n sleep: Sleep = defaultSleep,\n): Promise<PackageUploadResult> {\n const publicUrl = nonEmpty(file.publicUrl);\n if (publicUrl) return { fileId: file.id, downloadUrl: publicUrl };\n\n if (!options.requirePublicUrl) {\n const downloadUrl = nonEmpty(file.downloadUrl);\n if (!downloadUrl) {\n throw new Error(\"completeUpload did not return a usable URL\");\n }\n return { fileId: file.id, downloadUrl };\n }\n\n const delays =\n options.publicUrlRetryDelaysMs ?? DEFAULT_PUBLIC_URL_RETRY_DELAYS_MS;\n for (const delay of delays) {\n await sleep(delay);\n let retried: string | undefined;\n try {\n retried = await fetchPublicUrl(gql, file.id);\n } catch {\n // A failed re-read is treated like \"not ready yet\"; the next attempt\n // (or the final error below) covers it.\n retried = undefined;\n }\n if (retried) return { fileId: file.id, downloadUrl: retried };\n }\n throw new Error(PUBLIC_URL_NOT_READY_MESSAGE);\n}\n\nexport function usePackageUpload(\n workspacesGatewayUrl: string | undefined,\n authToken: string | undefined,\n workspaceId: string | undefined,\n getWorkspaceToken: (() => Promise<string | null>) | undefined,\n folderPath = PACKAGE_UPLOAD_FOLDER,\n options: PackageUploadOptions = {},\n) {\n const { requirePublicUrl = false, publicUrlRetryDelaysMs } = options;\n // Key the retry schedule by value, not identity, so a caller passing an\n // inline array literal does not recreate `upload` on every render.\n const retryDelaysKey = publicUrlRetryDelaysMs?.join(\",\");\n const retryDelays = useMemo<readonly number[] | undefined>(\n () =>\n retryDelaysKey === undefined\n ? undefined\n : retryDelaysKey\n .split(\",\")\n .filter((part) => part !== \"\")\n .map(Number),\n [retryDelaysKey],\n );\n const [state, setState] = useState<PackageUploadState>({\n uploading: false,\n progress: 0,\n error: null,\n });\n\n const upload = useCallback(\n async (file: File): Promise<PackageUploadResult> => {\n if (!workspacesGatewayUrl) {\n throw new Error(\n \"workspacesGatewayUrl is not configured. Pass it as a prop to DevportalRoot.\",\n );\n }\n\n const freshToken = getWorkspaceToken\n ? await getWorkspaceToken()\n : undefined;\n const gql = makeFetcher(\n workspacesGatewayUrl,\n authToken,\n workspaceId,\n freshToken ?? undefined,\n );\n\n setState({ uploading: true, progress: 0, error: null });\n try {\n const folderId = await ensureFolder(gql, folderPath);\n setState((s) => ({ ...s, progress: 5 }));\n\n const { sessionId, uploadUrl } = await initiateUpload(\n gql,\n file,\n folderId,\n );\n setState((s) => ({ ...s, progress: 10 }));\n\n await putToPresignedUrl(file, uploadUrl, (pct) =>\n setState((s) => ({ ...s, progress: pct })),\n );\n setState((s) => ({ ...s, progress: 90 }));\n\n const uploaded = await completeUpload(\n gql,\n sessionId,\n folderId,\n file.size,\n );\n const result = await resolvePersistableUrl(gql, uploaded, {\n requirePublicUrl,\n publicUrlRetryDelaysMs: retryDelays,\n });\n setState({ uploading: false, progress: 100, error: null });\n return result;\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Upload failed\";\n setState({ uploading: false, progress: 0, error: message });\n throw err;\n }\n },\n [\n workspacesGatewayUrl,\n authToken,\n workspaceId,\n getWorkspaceToken,\n folderPath,\n requirePublicUrl,\n retryDelays,\n ],\n );\n\n return { upload, ...state };\n}\n\nconst ASSET_UPLOAD_OPTIONS: PackageUploadOptions = { requirePublicUrl: true };\n\n/**\n * Upload hook for store listing images (icons, banners, screenshots). Stores\n * under ASSET_UPLOAD_FOLDER and only ever returns the permanent publicUrl.\n */\nexport function useAssetUpload(\n workspacesGatewayUrl: string | undefined,\n authToken: string | undefined,\n workspaceId: string | undefined,\n getWorkspaceToken: (() => Promise<string | null>) | undefined,\n) {\n return usePackageUpload(\n workspacesGatewayUrl,\n authToken,\n workspaceId,\n getWorkspaceToken,\n ASSET_UPLOAD_FOLDER,\n ASSET_UPLOAD_OPTIONS,\n );\n}\n"],"mappings":"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"}