Skip to content

host and upload images#2

Merged
gorrawith merged 2 commits intomainfrom
hosting-images
Mar 26, 2026
Merged

host and upload images#2
gorrawith merged 2 commits intomainfrom
hosting-images

Conversation

@gorrawith
Copy link
Copy Markdown
Owner

@gorrawith gorrawith commented Mar 26, 2026

Summary by CodeRabbit

  • New Features
    • Projects now persist and display dynamically in a list on the home page with thumbnail previews and timestamps
    • Uploaded projects are accessible through the project viewer, displaying project name and source image
    • Images are automatically processed, uploaded, and made accessible through the hosting system
    • Project data including names, images, and metadata is retained across sessions

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

This PR adds project creation and persistence capabilities by introducing hosting integration, a new createProject action for storing projects with uploaded images, and dynamic project listing on the home page. The visualizer route is updated to accept navigation state, and type definitions are extended to support the new domain model.

Changes

Cohort / File(s) Summary
Home & Visualizer Routes
app/routes/home.tsx, app/routes/visualizer.$id.tsx
Home component now manages projects state and calls createProject on upload completion, then navigates with route state including initial images and project name. Visualizer reads route state via useLocation() and conditionally displays project name and source image.
Project Persistence
lib/puter.action.ts
Added new exported createProject() action that retrieves hosting config, conditionally uploads source/rendered images to hosting, resolves final image URLs (preferring hosted uploads, then checking if already hosted), constructs a project payload excluding local paths, and returns the saved design item or null on failure.
Hosting Integration
lib/puter.hosting.ts
New module providing getOrCreateHostingConfig() to manage hosting subdomain storage in KV, and uploadImageToHosting() to upload images (with PNG conversion for rendered images) to a projects/${projectId} directory structure and return hosted URLs.
Utility Functions
lib/utils.ts
New module exporting hosting utilities: isHostedUrl() type guard, createHostingSlug() for unique subdomain generation, getHostedUrl() for URL construction, getImageExtension() for MIME/path-based extension detection, dataUrlToBlob() and fetchBlobFromUrl() for blob conversion, and imageUrlToPngBlob() for canvas-based PNG encoding.
Type System
type.d.ts
Standardized AuthState and AuthContext delimiters (commas to semicolons), added domain model types (Material, DesignItem, DesignConfig, AppStatus), and introduced parameter/state types for hosting (HostingConfig, HostedAsset, StoreHostedImageParams, CreateProjectParams), navigation (VisualizerLocationState), and component props.

Sequence Diagram

sequenceDiagram
    participant User
    participant Home as Home Component
    participant Upload as Upload Handler
    participant CreateProject as createProject Action
    participant Hosting as Hosting Module
    participant KV as Puter KV Store
    participant FileSystem as Puter FileSystem
    participant Visualizer as Visualizer Route

    User->>Home: Upload image
    Home->>Upload: handleUploadComplete()
    Upload->>CreateProject: createProject({ item })
    CreateProject->>KV: getOrCreateHostingConfig()
    KV-->>CreateProject: { subdomain }
    CreateProject->>Hosting: uploadImageToHosting(sourceImage)
    Hosting->>FileSystem: Write sourceImage to projects/{id}
    FileSystem-->>Hosting: File path
    Hosting-->>CreateProject: { url: hostedUrl }
    CreateProject->>Hosting: uploadImageToHosting(renderedImage)
    Hosting->>FileSystem: Write renderedImage to projects/{id}
    FileSystem-->>Hosting: File path
    Hosting-->>CreateProject: { url: hostedUrl }
    CreateProject->>KV: Save project payload
    KV-->>CreateProject: Success
    CreateProject-->>Upload: DesignItem
    Upload->>Home: Update projects state
    Home->>Visualizer: navigate(/visualizer/{id}, state)
    Visualizer-->>User: Display project with initial image
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • implement drag and drop uploader #1: Introduces the initial Home and Visualizer route structure; this PR extends that foundation by wiring upload completion to project creation and adding route state navigation.

Poem

🐰 A project is born from each uploaded dream,
Hosted images flow in a seamless stream,
State travels swift through the visualizer's door,
Where projects now live and are kept evermore!
Hopping with joy, the rabbit does cheer! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'host and upload images' accurately reflects the main changes: new hosting infrastructure (lib/puter.hosting.ts, lib/utils.ts), image upload logic (createProject in lib/puter.action.ts), and integration into the Home component for project creation with image uploads.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hosting-images

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
lib/puter.hosting.ts (1)

29-29: Misleading error message.

The message says "Could not find subdomain" but the error occurs during creation, not lookup. Consider clarifying:

-        console.warn(`Could not find subdomain: ${e}`);
+        console.warn(`Failed to create hosting subdomain: ${e}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/puter.hosting.ts` at line 29, The console.warn call logging an error as
"Could not find subdomain" is misleading because the failure happens during
creation; update the log in the block that calls console.warn (the
console.warn(`Could not find subdomain: ${e}`) statement) to clearly state
creation failed (e.g., "Could not create subdomain") and include the error
details (use the existing error variable e) so the message reads something like
"Could not create subdomain: <error>" for accurate debugging context.
lib/puter.action.ts (1)

38-42: Redundant optional chaining.

hostedRender?.url is checked twice unnecessarily. The logic can be simplified.

♻️ Simplified version
-    const resolvedRender = hostedRender?.url
-        ? hostedRender?.url
-        : item.renderedImage && isHostedUrl(item.renderedImage)
-            ? item.renderedImage
-            : undefined;
+    const resolvedRender = hostedRender?.url
+        || (item.renderedImage && isHostedUrl(item.renderedImage) ? item.renderedImage : undefined);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/puter.action.ts` around lines 38 - 42, The conditional for resolvedRender
redundantly checks hostedRender?.url twice; simplify by first testing
hostedRender && hostedRender.url (or hostedRender.url) once and falling back to
item.renderedImage when isHostedUrl(item.renderedImage) is true. Update the
expression that assigns resolvedRender (reference: resolvedRender,
hostedRender.url, item.renderedImage, isHostedUrl) to remove the duplicate
optional chaining and use a single conditional branch that returns
hostedRender.url if present otherwise item.renderedImage when isHostedUrl(...)
else undefined.
app/routes/visualizer.$id.tsx (1)

3-5: Route parameter $id is unused.

The route is defined as visualizer.$id.tsx but the id parameter from the URL is never extracted or used. Currently, the component relies solely on location.state, which means:

  1. Direct URL access (e.g., refreshing the page or sharing the link) will show "Untitled Project" with no image
  2. The URL parameter provides no value

Consider extracting and using the route param to fetch project data from storage for a more robust implementation.

♻️ Suggested approach to use the route parameter
-import {useLocation} from "react-router";
+import {useLocation, useParams} from "react-router";

 const VisualizerId = () => {
+    const { id } = useParams();
     const location = useLocation();
     const { initialImage, name } = location.state || {};
+
+    // TODO: If no location.state, fetch project by id from storage
+    // This enables direct URL access and page refresh support
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/visualizer`.$id.tsx around lines 3 - 5, The component VisualizerId
reads initialImage and name from location.state but never reads the route
parameter $id, so direct URL access loses data; update VisualizerId to extract
the id using useParams (or the framework's route params helper) and then load
project data when id is present (either by calling a data loader like
fetchProjectById/projectStore.get(id) or invoking a useEffect that fetches and
sets initialImage/name), falling back to location.state when available;
reference the VisualizerId component, useParams (or loader), and the
fetchProjectById/projectStore.get function names when making the change.
app/routes/home.tsx (2)

114-116: Hardcoded values should be dynamic.

The "Community" badge and "By JS Mastery" author text are hardcoded. These should reflect actual project visibility and ownership:

  • The badge shows "Community" for all projects, including private ones
  • The author is hardcoded instead of using actual user/owner information
♻️ Suggested approach
                              <div className="badge">
-                                 <span>Community</span>
+                                 <span>{isPublic ? 'Community' : 'Private'}</span>
                              </div>
-                                         <span>By JS Mastery</span>
+                                         <span>By {sharedBy || 'You'}</span>

Also applies to: 126-126

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/home.tsx` around lines 114 - 116, Replace the hardcoded
"Community" badge and "By JS Mastery" author text with dynamic values from the
project data: use the project's visibility property (e.g., project.visibility or
project.isPrivate) to render the badge label and appropriate styling in the div
with className "badge", and use the project's owner info (e.g.,
project.owner.name, project.owner.username or project.ownerId lookup) to
populate the author string where "By JS Mastery" is rendered; ensure you handle
missing owner data by falling back to a sensible default and update any
tests/components that expect the static text (references: the JSX elements
containing <div className="badge"><span>Community</span></div> and the "By JS
Mastery" author text).

19-19: Projects state is ephemeral — no persistence on page reload.

The projects state starts empty and is only populated when new uploads occur. Existing projects are not loaded from storage on component mount, meaning:

  • Page refresh clears all displayed projects
  • Previously created projects are inaccessible from the home page

Consider adding a useEffect to fetch existing projects from storage on mount.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/home.tsx` at line 19, The projects state initialized by
useState<DesignItem[]>([]) is ephemeral and should be hydrated on mount; add a
useEffect in the same component (the file containing the const [projects,
setProjects]) that reads persisted projects (e.g., from localStorage or your
existing persistence API), safely JSON.parse's the stored value (handling parse
errors and missing keys), and calls setProjects(parsedProjects as DesignItem[]);
also ensure any save/upload paths that currently persist new projects write to
the same storage key so the loaded data matches what setProjects expects.
lib/utils.ts (1)

91-102: CORS handling may fail for some image sources.

Setting crossOrigin = "anonymous" enables CORS requests, but if the server doesn't include appropriate CORS headers, the image will fail to load. Additionally, for data URLs, setting crossOrigin can cause issues in some browsers.

Consider adding a fallback or handling the data URL case separately.

♻️ Handle data URLs without CORS
     try {
         const img = new Image();
-        img.crossOrigin = "anonymous";
+        // Only set crossOrigin for non-data URLs to avoid CORS issues
+        if (!url.startsWith("data:")) {
+            img.crossOrigin = "anonymous";
+        }

         const loaded = await new Promise<HTMLImageElement>((resolve, reject) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/utils.ts` around lines 91 - 102, imageUrlToPngBlob currently always sets
img.crossOrigin = "anonymous", which breaks data URLs and images from origins
without CORS; update the function to detect data URLs and load them without
setting crossOrigin, and add a fallback path that, if the initial load rejects,
retries creating/loading the Image without any crossOrigin value (or unset
crossOrigin) before giving up; reference the imageUrlToPngBlob function and the
img.crossOrigin usage and ensure the Promise retry logic handles both the data
URL branch and the no-CORS retry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/routes/home.tsx`:
- Around line 108-109: The JSX returned by projects.map in the Home route omits
a React key, causing reconciliation warnings; update the map callback that
renders the <div className="project-card group"> (inside projects.map) to
include a stable key prop (e.g., key={id}) on the root element for each item,
falling back to a deterministic alternative only if id can be absent, to ensure
unique keys during rendering.

In `@lib/puter.action.ts`:
- Around line 57-60: The try block in the createProject flow returns payload but
never persists project metadata; call puter.kv.set(...) to store the project
(use the same key pattern used in lib/puter.hosting.ts) and await the result
before returning payload, handling any errors in the catch; reference the
payload object and the puter.kv.set method so the project record is created when
createProject finishes.
- Line 17: The CreateProjectParams type declares a visibility property but
createProject currently only destructures { item }, dropping visibility; update
createProject to also destructure visibility (e.g., change signature to accept {
item, visibility }) and apply it to the created DesignItem (or otherwise use it
in the project-creation flow), or if visibility is truly unused remove
visibility from CreateProjectParams and all callers; key symbols to modify are
the CreateProjectParams type and the createProject function to ensure visibility
is either consumed (set on item / passed to underlying persistence logic) or
removed entirely.

In `@lib/puter.hosting.ts`:
- Around line 11-31: getOrCreateHostingConfig has a TOCTOU race: multiple
concurrent calls can each create different subdomains before puter.kv.set,
causing overwrites; fix by introducing a module-level promise cache (e.g.,
hostingConfigPromise) so concurrent callers share a single in-flight creation.
In getOrCreateHostingConfig, if hostingConfigPromise exists return/await it;
otherwise assign hostingConfigPromise = (async () => { check
puter.kv.get(HOSTING_CONFIG_KEY) again, if present return it, else call
createHostingSlug() and puter.hosting.create(...), then
puter.kv.set(HOSTING_CONFIG_KEY, record); return record })(); ensure you clear
or preserve hostingConfigPromise appropriately on failures so future calls can
retry.

---

Nitpick comments:
In `@app/routes/home.tsx`:
- Around line 114-116: Replace the hardcoded "Community" badge and "By JS
Mastery" author text with dynamic values from the project data: use the
project's visibility property (e.g., project.visibility or project.isPrivate) to
render the badge label and appropriate styling in the div with className
"badge", and use the project's owner info (e.g., project.owner.name,
project.owner.username or project.ownerId lookup) to populate the author string
where "By JS Mastery" is rendered; ensure you handle missing owner data by
falling back to a sensible default and update any tests/components that expect
the static text (references: the JSX elements containing <div
className="badge"><span>Community</span></div> and the "By JS Mastery" author
text).
- Line 19: The projects state initialized by useState<DesignItem[]>([]) is
ephemeral and should be hydrated on mount; add a useEffect in the same component
(the file containing the const [projects, setProjects]) that reads persisted
projects (e.g., from localStorage or your existing persistence API), safely
JSON.parse's the stored value (handling parse errors and missing keys), and
calls setProjects(parsedProjects as DesignItem[]); also ensure any save/upload
paths that currently persist new projects write to the same storage key so the
loaded data matches what setProjects expects.

In `@app/routes/visualizer`.$id.tsx:
- Around line 3-5: The component VisualizerId reads initialImage and name from
location.state but never reads the route parameter $id, so direct URL access
loses data; update VisualizerId to extract the id using useParams (or the
framework's route params helper) and then load project data when id is present
(either by calling a data loader like fetchProjectById/projectStore.get(id) or
invoking a useEffect that fetches and sets initialImage/name), falling back to
location.state when available; reference the VisualizerId component, useParams
(or loader), and the fetchProjectById/projectStore.get function names when
making the change.

In `@lib/puter.action.ts`:
- Around line 38-42: The conditional for resolvedRender redundantly checks
hostedRender?.url twice; simplify by first testing hostedRender &&
hostedRender.url (or hostedRender.url) once and falling back to
item.renderedImage when isHostedUrl(item.renderedImage) is true. Update the
expression that assigns resolvedRender (reference: resolvedRender,
hostedRender.url, item.renderedImage, isHostedUrl) to remove the duplicate
optional chaining and use a single conditional branch that returns
hostedRender.url if present otherwise item.renderedImage when isHostedUrl(...)
else undefined.

In `@lib/puter.hosting.ts`:
- Line 29: The console.warn call logging an error as "Could not find subdomain"
is misleading because the failure happens during creation; update the log in the
block that calls console.warn (the console.warn(`Could not find subdomain:
${e}`) statement) to clearly state creation failed (e.g., "Could not create
subdomain") and include the error details (use the existing error variable e) so
the message reads something like "Could not create subdomain: <error>" for
accurate debugging context.

In `@lib/utils.ts`:
- Around line 91-102: imageUrlToPngBlob currently always sets img.crossOrigin =
"anonymous", which breaks data URLs and images from origins without CORS; update
the function to detect data URLs and load them without setting crossOrigin, and
add a fallback path that, if the initial load rejects, retries creating/loading
the Image without any crossOrigin value (or unset crossOrigin) before giving up;
reference the imageUrlToPngBlob function and the img.crossOrigin usage and
ensure the Promise retry logic handles both the data URL branch and the no-CORS
retry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 412f9f67-89ad-49da-9aac-04c12feba069

📥 Commits

Reviewing files that changed from the base of the PR and between 93f70c2 and 43417b7.

📒 Files selected for processing (6)
  • app/routes/home.tsx
  • app/routes/visualizer.$id.tsx
  • lib/puter.action.ts
  • lib/puter.hosting.ts
  • lib/utils.ts
  • type.d.ts

Comment thread app/routes/home.tsx
Comment on lines +108 to +109
{projects.map(({id, name, renderedImage, sourceImage, timestamp}) => (
<div className="project-card group">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing key prop in list rendering.

The projects.map() renders items without a key prop, which will cause React reconciliation issues and trigger a console warning.

🔧 Proposed fix
-                     {projects.map(({id, name, renderedImage, sourceImage, timestamp}) => (
-                         <div className="project-card group">
+                     {projects.map(({id, name, renderedImage, sourceImage, timestamp}) => (
+                         <div key={id} className="project-card group">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{projects.map(({id, name, renderedImage, sourceImage, timestamp}) => (
<div className="project-card group">
{projects.map(({id, name, renderedImage, sourceImage, timestamp}) => (
<div key={id} className="project-card group">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/home.tsx` around lines 108 - 109, The JSX returned by projects.map
in the Home route omits a React key, causing reconciliation warnings; update the
map callback that renders the <div className="project-card group"> (inside
projects.map) to include a stable key prop (e.g., key={id}) on the root element
for each item, falling back to a deterministic alternative only if id can be
absent, to ensure unique keys during rendering.

Comment thread lib/puter.action.ts
}
}

export const createProject = async ({ item }: CreateProjectParams): Promise<DesignItem | null | undefined> => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The visibility parameter is declared in CreateProjectParams but never used.

The function destructures only { item }, silently dropping the visibility property that callers (e.g., home.tsx line 31) pass. Either implement visibility logic or remove it from the interface.

🔧 Option A: Use the parameter
-export const createProject = async ({ item }: CreateProjectParams): Promise<DesignItem | null | undefined> => {
+export const createProject = async ({ item, visibility = 'private' }: CreateProjectParams): Promise<DesignItem | null> => {
     const projectId = item.id;
+    // Use visibility when storing...
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const createProject = async ({ item }: CreateProjectParams): Promise<DesignItem | null | undefined> => {
export const createProject = async ({ item, visibility = 'private' }: CreateProjectParams): Promise<DesignItem | null> => {
const projectId = item.id;
// Use visibility when storing...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/puter.action.ts` at line 17, The CreateProjectParams type declares a
visibility property but createProject currently only destructures { item },
dropping visibility; update createProject to also destructure visibility (e.g.,
change signature to accept { item, visibility }) and apply it to the created
DesignItem (or otherwise use it in the project-creation flow), or if visibility
is truly unused remove visibility from CreateProjectParams and all callers; key
symbols to modify are the CreateProjectParams type and the createProject
function to ensure visibility is either consumed (set on item / passed to
underlying persistence logic) or removed entirely.

Comment thread lib/puter.action.ts
Comment on lines +57 to +60
try {
// Call the Puter worker to store project in kv

return payload;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Project persistence is not implemented.

The try block contains only a comment indicating storage should occur, but no actual persistence call exists. The function uploads images but never stores the project metadata, making createProject misleading — it hosts images but doesn't create a persistent project record.

Based on the pattern in lib/puter.hosting.ts (line 23), the implementation should call puter.kv.set() to persist the project.

🔧 Suggested implementation
     try {
         // Call the Puter worker to store project in kv
-
+        await puter.kv.set(`project:${projectId}`, payload);
         return payload;
     } catch (e) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/puter.action.ts` around lines 57 - 60, The try block in the createProject
flow returns payload but never persists project metadata; call puter.kv.set(...)
to store the project (use the same key pattern used in lib/puter.hosting.ts) and
await the result before returning payload, handling any errors in the catch;
reference the payload object and the puter.kv.set method so the project record
is created when createProject finishes.

Comment thread lib/puter.hosting.ts
Comment on lines +11 to +31
export const getOrCreateHostingConfig = async (): Promise<HostingConfig | null> => {
const existing = (await puter.kv.get(HOSTING_CONFIG_KEY)) as HostingConfig | null;

if(existing?.subdomain) return { subdomain: existing.subdomain };

const subdomain = createHostingSlug();

try {
const created = await puter.hosting.create(subdomain, '.');

const record = { subdomain: created.subdomain };

await puter.kv.set(HOSTING_CONFIG_KEY,record);

return record;


} catch (e) {
console.warn(`Could not find subdomain: ${e}`);
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition in getOrCreateHostingConfig.

This function has a TOCTOU (time-of-check-time-of-use) vulnerability. Between checking puter.kv.get() and calling puter.kv.set(), another concurrent call could:

  1. Also see no existing config
  2. Create its own subdomain
  3. Overwrite the first call's stored config

This can occur when a user rapidly uploads multiple images before the first createProject completes.

🔒 Suggested mitigation using module-level promise caching
+let hostingConfigPromise: Promise<HostingConfig | null> | null = null;
+
-export const getOrCreateHostingConfig = async (): Promise<HostingConfig | null> => {
+export const getOrCreateHostingConfig = (): Promise<HostingConfig | null> => {
+    if (hostingConfigPromise) return hostingConfigPromise;
+
+    hostingConfigPromise = (async () => {
         const existing = (await puter.kv.get(HOSTING_CONFIG_KEY)) as HostingConfig | null;
-
         if(existing?.subdomain) return { subdomain: existing.subdomain };

         const subdomain = createHostingSlug();

         try {
             const created = await puter.hosting.create(subdomain, '.');
-
             const record = { subdomain: created.subdomain };
-
             await puter.kv.set(HOSTING_CONFIG_KEY, record);
-
             return record;
-
-        
         } catch (e) {
             console.warn(`Could not find subdomain: ${e}`);
+            hostingConfigPromise = null; // Allow retry on failure
             return null;
         }
-}
+    })();
+
+    return hostingConfigPromise;
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/puter.hosting.ts` around lines 11 - 31, getOrCreateHostingConfig has a
TOCTOU race: multiple concurrent calls can each create different subdomains
before puter.kv.set, causing overwrites; fix by introducing a module-level
promise cache (e.g., hostingConfigPromise) so concurrent callers share a single
in-flight creation. In getOrCreateHostingConfig, if hostingConfigPromise exists
return/await it; otherwise assign hostingConfigPromise = (async () => { check
puter.kv.get(HOSTING_CONFIG_KEY) again, if present return it, else call
createHostingSlug() and puter.hosting.create(...), then
puter.kv.set(HOSTING_CONFIG_KEY, record); return record })(); ensure you clear
or preserve hostingConfigPromise appropriately on failures so future calls can
retry.

@gorrawith gorrawith merged commit c2af551 into main Mar 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant