Compare commits

...
Author SHA1 Message Date
tamilselvanp57 a4bb57bb79 Fix static hosting domain fallback for self-hosted custom domains (#3382) 2026-07-15 11:45:10 -07:00
Daniel Salazar 861cf5efb8 feat: email api wip (#3391) 2026-07-15 11:30:16 -07:00
Daniel Salazar cf34f9fea9 fix: sanitize get user response (#3390)
* fix: sanitize get user response

* fix: app creation in dev center
2026-07-15 02:02:06 -07:00
jelveh 92a6d30e26 Add calendar to recommended apps 2026-07-14 23:04:24 -07:00
Neal ShahandDaniel Salazar 23e8705b03 Implement workers without cloudflare for local dev testing (#3389)
* typeify subdomains wip

* initial (untested) logic for LocalWorkerService

* Make it work, add lifecycle expiry since workers are process heavy in current implementation

* fix type errors

---------

Co-authored-by: Daniel Salazar <[email protected]>
2026-07-15 01:01:42 -04:00
Daniel Salazar 3240a4670a fix: admin gates (#3386) 2026-07-14 12:58:04 -07:00
Daniel Salazar 3bf83d40e9 fix: ui signup order (#3385) 2026-07-13 18:47:11 -07:00
Neal Shah c3368f553e Add GUI button for custom OIDC (#3384) 2026-07-13 20:32:37 -04:00
velzie 27def94d8b feat: support cross-origin-isolated login (#3338) 2026-07-13 17:36:56 -04:00
Neal Shah 2c139929ac Check if user is suspended on WebDAV basic auth and others (#3369) 2026-07-13 02:46:28 -04:00
Neal Shah b957f797da add support for ndjson logging (#3381)
* add support for ndjson logging

* move install location
2026-07-13 02:43:56 -04:00
jelveh 393500d565 Add calculator to recommended apps
Include 'calculator' in RECOMMENDED_APP_NAMES within RecommendedAppsService.ts so the calculator app is included in the recommended apps list.
2026-07-11 23:04:39 -07:00
jelveh a108837e82 Revert "Add delayed centered spinner for apps tab"
This reverts commit 859e88753f.
2026-07-11 15:21:59 -07:00
jelveh 859e88753f Add delayed centered spinner for apps tab
Show a subtle centered loading spinner for the Apps dashboard when fetching apps if the request is slow. Implements a 1.5s delay before showing the spinner and guarantees a minimum 1.5s visible time to avoid blinking; the spinner is only shown when the container is empty (so background refreshes keep existing tiles). Adds a finish helper to clear the spinner timer and await the minimum visible time before rendering results or errors. Also adds CSS for the spinner element and its animations.
2026-07-11 15:16:04 -07:00
Daniel Salazar 5160b44c4b fix: missing OIDC error messages (#3377)
* fix: missing OIDC error messages

* fix: card verification user prefill out for subs
2026-07-11 08:18:13 -07:00
Daniel Salazar 17a9485fb2 Add spans to drivers and high frequency paths (#3376) 2026-07-10 23:15:09 -07:00
Daniel Salazar d3c44f3d6c fix: cache healthcheck in mem for each node (#3375) 2026-07-10 17:41:09 -07:00
Neal Shah d49ba5281d add support for ignoring specific issues in healthcheck (#3374)
* add support for ignoring specific issues in healthcheck

* add mark-degraded to healthcheck
2026-07-10 17:08:03 -04:00
jelveh c107c80bd8 Open dashboard links at #home anchor 2026-07-10 13:22:58 -07:00
Daniel Salazar cf90e775bf fingerprint: use device info for rate limit too (#3372) 2026-07-09 20:22:56 -07:00
Daniel Salazar 0a46096c41 fix: ai rate limits (#3371) 2026-07-09 16:35:03 -07:00
Daniel Salazar 289a44ae79 fix: OIDC state (#3370) 2026-07-09 14:47:34 -07:00
Neal Shah 827e9bc343 fix possible XSS entrypoints (#3368) 2026-07-09 17:19:55 -04:00
Daniel Salazar f2ffaeb823 fix: add phone errors into kv for debugging (#3367) 2026-07-09 12:25:17 -07:00
Neal Shah 6f594f4b60 add 5.6 models (#3366) 2026-07-09 14:46:57 -04:00
jelveh 326be042f1 Merge branch 'main' of https://github.com/HeyPuter/puter 2026-07-08 22:33:20 -07:00
jelveh 5b824fb745 Extract resolveTileDisplay and broaden app search
Move logic that determines a tile's displayed title and target link into resolveTileDisplay (use hostname/index_url for external apps) and have buildTileHtml consume it. Expose data-target-link on tiles. Update app search to match the displayed title, raw title, name, and uid so queries find website-shortcut tiles and external apps consistently (matching Home tab behavior).
2026-07-08 22:33:13 -07:00
Reynaldi Chernando cf6cb126b5 fix grok 4.5 release date (#3365) 2026-07-09 12:30:39 +07:00
Reynaldi ChernandoandCopilot Autofix powered by AI 302df1edd2 update user pays model docs (#3364)
* update user pays model docs

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-07-09 08:54:04 +07:00
jelveh e212509ed3 Avoid clipping scaled icons in horizontal scroller
Add a negative top margin and matching top padding to the horizontal scroller to prevent first-row icons from being clipped when they scale on hover. Overflow-x:auto previously caused vertical clipping of scaled icons; margin-top:-8px and padding-top:8px bleed the clip edge upward so scaled icons remain visible without changing scroll behavior.
2026-07-08 18:35:57 -07:00
Daniel Salazar d3e3755d6d chore: cleanup uneeded ai messages (#3363) 2026-07-08 17:55:49 -07:00
jelveh 948ce6e529 Merge branch 'main' of https://github.com/HeyPuter/puter 2026-07-08 17:51:38 -07:00
jelveh d117dc231e Implement paged apps launcher UI 2026-07-08 17:50:34 -07:00
Daniel Salazar eefa53e5bd fix: misc auth bug fixes (#3362) 2026-07-08 17:50:22 -07:00
Neal Shah db89fbfc52 add grok 4.5 (#3360) 2026-07-08 14:44:05 -04:00
Daniel Salazar 97ec5d115c fix: double counting purchased credit usage (#3359) 2026-07-07 21:43:09 -07:00
Daniel Salazar 722a6cf50f fix: phone retry limit messagin (#3358) 2026-07-07 21:05:41 -07:00
Neal Shah d039dcfee5 fix appTelemetry (#3357) 2026-07-07 20:10:13 -04:00
Daniel Salazar d09aa11f2e feat: add whatsapp support? (#3356) 2026-07-07 16:13:18 -07:00
jelveh 8a2264546d Always use hash for dashboard tabs
Previously the dashboard used a clean root URL for the 'apps' tab and hashes for other tabs. This change makes tab links and history entries consistently use a hash (e.g. #apps, #home) by always generating href as `#<id>` and pushing `#<section>` to history. Simplifies URL handling and ensures the Apps tab also reflects its hash in the address bar.
2026-07-07 15:16:59 -07:00
0801e1dc44 feat: add signup disable config and fix telemetry startup (#3319)
* feat: add signup disable config and fix telemetry startup

* fix: nits for config spread and 403 checks

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Daniel Salazar <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-07 13:52:29 -07:00
Daniel Salazar a04d5fa5e0 fix: package-lock (#3355) 2026-07-07 13:43:15 -07:00
e1280e8cc7 Add custom icon support for web links (#3325)
* feat: add custom web link icon support and context menu option

Introduce the ability to change and display custom icons for .weblink files. Added a "Change Icon" context menu item that invokes the changeWeblinkIcon helper, and updated the icon rendering logic to use getWeblinkIcon instead of the default link icon. This allows users to personalize web link appearance in the file manager.

* Fix security and robustness issues in web link custom icons

 # UIWindowSearch and require the icon data URL body to be pure base64 in
  #isValidWeblinkIcon. A shared/downloaded .weblink is untrusted input and
 # the previous prefix-only check let a crafted icon break out of the
 # <img src> attribute.
 # byte-signature MIME sniffing and capping stored icon size (was storing
 # full-resolution images as base64).
 # without picking a file.
 # weblinks no longer fetches every file's contents on each render.
 # path instead of the target weblink).
 # label, add the AGPL license header, and drop redundant stored icon
 # copies.

* Keep SVG weblink icons as vectors instead of rasterizing

Rasterizing a picked SVG to a 256px PNG threw away its scalability for no
real benefit: an SVG rendered in <img> runs in script-free static mode, and
the base64-body validation already prevents attribute injection. Store SVGs
as data:image/svg+xml;base64 (content-sniffed, size-capped), and keep
rasterizing raster formats and oversized SVGs to bound stored size.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Daniel Salazar <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-07 13:41:37 -07:00
Aman Rai 70884c2e30 fix: rename start-gui script to start:gui (#3346) 2026-07-07 12:09:49 -07:00
Reynaldi Chernando 917e95e3d7 Add nano banana 2 lite (#3336) 2026-07-07 12:08:36 -07:00
jelvehandClaude Opus 4.8 27222bbb0d Let ctrl/cmd+f use browser find while dashboard is open
The global ctrl/cmd+f shortcut opened UIWindowSearch and preventDefault'd
the keystroke, blocking the browser's native find. Skip that interception
while a dashboard window is open so ctrl/cmd+f falls through to the browser.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-07 09:43:34 -07:00
jelveh 3720034464 Make Apps the default dashboard tab
Treat the Apps tab as the default (root URL) instead of Home. UIDashboard now reads an initial route from window.dashboard_initial_route?.tab and falls back to 'apps' for unknown/absent routes. Sidebar and section active class logic and tab href/hash generation were updated so 'apps' uses the clean root URL while all other tabs (including home) use #tab. The history.pushState logic was adjusted accordingly. parseDashboardRoute in initgui.js was updated to default to 'apps' and its comment clarified the new behavior.
2026-07-07 09:31:43 -07:00
jelveh de6cab1f9a Reduce myapps tile icon size to 56px
Update .myapps-tile-icon in src/gui/src/css/dashboard.css to change width and height from 60px to 56px, adjusting tile icon sizing for improved layout and spacing.
2026-07-06 21:04:32 -07:00
jelveh 2d1fbc2611 Auto-focus My Apps search on activation
Call focusSearch when the Apps dashboard is activated and add a focusSearch helper. The helper imports isTouchPrimaryDevice and skips focusing on touch-primary devices to avoid showing the on-screen keyboard. It retries up to 12 times (30ms interval) until the search input is visible to handle cases where the dashboard is briefly hidden while entering full-page mode.
2026-07-06 20:10:44 -07:00
jelveh 0afc70218f Use input[type=search] and normalize styling 2026-07-06 20:03:48 -07:00
jelveh 8d3abbc5f0 Increase myapps tile icon size to 60px
Update .myapps-tile-icon width and height from 52px to 60px to improve icon visibility and better match the updated UI design. Other styles (overflow, background, transitions) are left unchanged.
2026-07-06 19:47:17 -07:00
jelveh 7ec14bc7db Add audio-editor and ai-image-project to recommended apps 2026-07-06 19:42:20 -07:00
jelveh 2843010a97 Increase myapps tile size to 100px 2026-07-06 19:37:46 -07:00
jelveh ad4e21ee61 Open external apps via index_url from Apps tab
When an app is external and provides an index_url, use its hostname as the tile title and store the index_url in a new data-target-link attribute. Update the click handler to open the target link in a new tab for external apps, and fall back to opening the internal /app/{name} page for others. This aligns Apps tab behavior with the Home tab and ensures external apps navigate to their website.
2026-07-06 19:21:34 -07:00
Nariman Jelveh 4de39ab6f2 Mark external apps and use the flag for dashboard titles (#3354)
* Mark external apps and use the flag for dashboard titles

An app with no owner_user_id (null/empty) isn't owned by a Puter user —
it's an external, origin-bootstrapped app whose uuid/name/title are all the
opaque app-… id. The dashboard used to detect these client-side by comparing
uuid/name/title and a name.startsWith('app-') check.

Expose an authoritative `external` flag from the API instead:

- /installedApps and /get-launch-apps now return `external`, derived from
  owner_user_id, and no longer leak the raw owner id.
- The dashboard (Home + Apps tabs) shows the index_url hostname for external
  apps based on `external` rather than the uuid/name/title heuristic.

Add/extend extension tests to cover the `external` flag and non-leak.

* Use hostname for opaque external app titles

Detect opaque external app IDs (when name === title === uid/uuid) and replace the displayed title with the hostname from index_url. Adds a uid/uuid fallback and tightens the external-app check in both the Apps and Home dashboard tabs so they behave consistently; preserves target_link for Home entries.

* Add external flag for apps without owner

Expose an `external` property in AppSummary (toAppSummary in RecommendedAppsService.ts) to mark apps that are not owned by a Puter user. The flag is set when `owner_user_id` is null or an empty string, allowing clients to identify origin-bootstrapped/external apps.
2026-07-06 18:00:56 -07:00
Nariman Jelveh fbb646b8eb Return index_url from /installedApps for dashboard app titles (#3352)
The dashboard derives a hostname title for anonymous (app-…) apps whose
uuid/name/title are all identical, falling back to the host of index_url.
That worked for launch/recent apps but not for apps that come only from
/installedApps, because the endpoint didn't return index_url.

Add apps.index_url to the SELECT (alongside apps.icon, relying on the
functional dependency on the apps.id primary key) and assert it in the
extension test.
2026-07-06 16:46:31 -07:00
Neal Shah 46f75ecd6f add type for puter.app.authenticated (#3353) 2026-07-06 18:38:30 -04:00
Daniel Salazar f68caa3bbd feat: allow optional card fallback for failed phone verifications (#3351) 2026-07-06 14:59:55 -07:00
jelveh 0109bf5b72 Use index_url hostname for anonymous app title
Detect anonymous apps whose name/title/uid are identical opaque ids (starting with 'app-') and, when index_url is available, replace the displayed title with the index_url hostname (matching the Home tab behavior). Also persist index_url into the mapped app object and make title a mutable variable to support the change.
2026-07-06 14:43:39 -07:00
Nariman Jelveh c69fb52d5c Make the dashboard the default interface at root (#3345)
* feat: make the dashboard the default interface at root

The dashboard now loads at / (with /dashboard kept as an alias), and the
desktop moves to a new /desktop route. An "Open Desktop" button pinned at
the bottom of the dashboard sidebar switches to the desktop.

Root URLs that carry a desktop-only flow keep booting the desktop:
auth popups (?embedded_in_popup=), app deep links (?app=), direct
downloads (?download=), fullpage mode (?puter.fullpage=), and iframe
embeds.

Desktop-context redirects are retargeted so desktop users stay on the
desktop: logout, the QR "continue on phone" link, the auth cover-page
logo click, and window close/hide URL resets now use /desktop. OIDC
login/signup gains a strictly whitelisted return_to param (/desktop or
/dashboard) so social login returns to the interface it started from.

Also: the dashboard strips ?ref= like the desktop does, ?auth_token=
cleanup preserves the path and tab hash, the logged-out session list
gets the wallpaper back, ?download= also works at /desktop, and the GUI
dev server serves /desktop and /dashboard.

* Add Open Desktop bento card and CSS

Replace the previous desktop-switch UI with a full-width "Open Desktop" bento card that links to /desktop. Removes the pinned sidebar Open Desktop button and the old desktop-switch button/handler, and adds new CSS variables and styles for the desktop card (icon background, shadow, surface/header colors), hover shadow and arrow transition, and layout/padding adjustments.

Changed files: TabHome.js (removed old switch, added anchor card), UIDashboard.js (removed sidebar button), dashboard.css (new desktop color vars and styles; removed legacy desktop-switch/button styles).

* Remove box-shadow from myapps search input

Delete the box-shadow declarations from input.myapps-search::placeholder in dashboard.css. This simplifies the placeholder styling (removes subtle and light shadow layers), likely to reduce visual noise or fix rendering inconsistency.

* Show index_url hostname for opaque app IDs

Only replace an app's title with the hostname from index_url when the app appears to use an opaque id. Add a check for names starting with 'app-' so the hostname is used only if title, name and uuid are identical and the name looks like an opaque app id. This avoids overwriting meaningful user-facing names that may coincidentally match the uuid and clarifies the intent in the comment.

* Update dashboard.css
2026-07-06 13:22:27 -07:00
Neal Shah 3a26f1f1c2 add admin moderation events table (#3348) 2026-07-06 11:49:09 -04:00
Daniel Salazar f1b94a5b69 fix: otel upgrade bump (#3328) 2026-07-06 08:42:25 -07:00
Neal Shah e3fce9191a emit puter.app.authenticated event (#3337) 2026-07-06 11:23:27 -04:00
Nariman Jelveh 2c0a1b9bbb Revert "feat: add sign-in flow for crossOriginIsolated contexts (#3253)" (#3335)
This reverts commit 6fff1fd75f.
2026-06-30 23:32:27 -07:00
jelveh 0d7417f858 Add 'contacts' to recommended apps
Include 'contacts' in the RECOMMENDED_APP_NAMES array so the Contacts app appears in the recommended apps list. Updated src/backend/services/apps/RecommendedAppsService.ts.
2026-06-30 19:08:11 -07:00
Neal Shah 2eee098c66 Add suspension reasons (#3331) 2026-06-30 18:09:00 -04:00
Neal Shah 890d4b3c16 remove temperature from sonnet-5 (#3330) 2026-06-30 17:18:29 -04:00
Neal Shah af9a7c6345 add claude sonnet 5 (#3329) 2026-06-30 17:08:42 -04:00
velzie 6fff1fd75f feat: add sign-in flow for crossOriginIsolated contexts (#3253)
* poc

* route login key through broadcastservice

* cleanup

* security validation and prevent additional webhook sends

* block double-broadcast from redis
2026-06-30 20:17:53 +01:00
velzie e6b7b08bda Add forceRelay option to Peer API and document raw peerconnection property (#3313)
* add forceRelay options

* document peerconnection
2026-06-30 12:28:49 -04:00
Daniel Salazar f4c3cbd53d feat: add logs for failed sms (#3318) 2026-06-30 06:50:47 -07:00
Reynaldi ChernandoandCopilot Autofix powered by AI c0c2f35266 Update docs model drift (#3326)
* Update docs model drift

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* standardize

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-06-30 16:55:42 +07:00
Nikhil Kumar SinghandDevblaze14 ccea980753 feat: add Ideogram 4.0 Together AI image model (#3309)
Co-authored-by: Devblaze14 <[email protected]>
2026-06-29 15:33:01 -04:00
Neal Shah 2bddebd576 add ability to suspend apps (#3322) 2026-06-29 13:06:08 -04:00
Neal Shah b9866ddbeb add suspended_at col (#3321) 2026-06-29 09:51:00 -04:00
Mochammad Fadhlan Al-Ghiffari 6fc79a7184 fix(docs): highlight code blocks after client-side navigation (#3287) (#3288)
* fix(docs): highlight all code blocks after client-side navigation

The docs site highlights code on full page load via an inline
hljs.highlightAll(), which targets the 'pre code' selector and so
highlights every fenced block, including those without a language tag.

The client-side navigation path (pathchange handler) instead selected
only code[class^="language"], so fenced blocks written without a
language hint were left unhighlighted after in-app navigation while
appearing correctly on a hard reload.

Align the pathchange handler with highlightAll() by selecting 'pre code'
and skipping blocks hljs has already processed (data-highlighted=yes),
keeping the pass idempotent across repeated pathchange events.

Fixes #3287

* fix(docs): hoist hljs.configure and scope highlight to docs content

Address review nits on the pathchange highlight pass:
- Move hljs.configure({ ignoreUnescapedHTML: true }) out of the per-element
  loop since it is global config and only needs to run once.
- Scope the selector to '.docs-content pre code' so the re-highlight pass
  only touches the swapped-in docs body and leaves code blocks elsewhere
  on the page untouched.
2026-06-28 23:27:07 +07:00
Nariman Jelveh 50039571b6 Reorder recommended apps and add vault
Adjust RECOMMENDED_APP_NAMES ordering: place 'editor', 'camera', and 'recorder' immediately after 'builder' and move 'app-center' accordingly. Also add 'vault' to the recommended apps list. This changes the resolved ordering of recommended apps at call time.
2026-06-27 17:55:52 -07:00
Daniel Salazar 1216f20567 fix: package-lock issue (#3316) 2026-06-27 14:33:11 -07:00
Daniel Salazar 0752ea761c feat: remove stych, add prelude client (#3312) 2026-06-27 14:07:52 -07:00
Daniel Salazar 98461088f8 fix: decrease puts for unused stamps (#3310) 2026-06-26 12:33:04 -07:00
Neal Shah d4a3224aa3 force locale en-US (#3311) 2026-06-26 15:31:58 -04:00
Daniel Salazar 197ce6faff fix: LegacyFSController (#3308) 2026-06-25 14:47:29 -07:00
MRB Labs c98e293879 fix: unlink OIDC providers when user updates email address (#3307) 2026-06-25 14:46:41 -07:00
Miika Kuisma ebbe382596 tweak context menu's text labels (#3306) 2026-06-25 09:46:05 -07:00
Neal Shah 01d28eb2c5 Await phone verification event (#3305) 2026-06-24 20:08:30 -04:00
Daniel Salazar 7d1c2e7f0a fix: fsService copy (#3304) 2026-06-24 16:32:34 -07:00
Daniel Salazar 22b499af20 fix: cleanup fs responses (#3303) 2026-06-24 14:35:58 -07:00
Neal Shah 7208f86723 bind btoa and atob in emulated worker globalspace (#3302)
* bind btoa and atob in emulated worker globalspace

* Create working directory for workers
2026-06-24 15:56:26 -04:00
Neal Shah 2974a8b01c store app owner ID even when worker is deployed from root ctx (#3301) 2026-06-24 15:21:48 -04:00
Daniel Salazar 7f05a707bd feat: improve kv store incr/decr costs (#3300) 2026-06-24 11:57:36 -07:00
Miika Kuismaandjelveh 1d056253ef contextmenu design tweak (#3298)
* contextmenu design tweak

* icon gap fix (mobile context menu web component)

* fix submenu item padding

* add dark theme support for puter.ui.contextMenu and puter.ui.setMenubar (works only with puter.env === 'web')

* Add theme test & refactor context menu items

Extract shared mediaMenuItems to remove duplication and simplify context menu calls. Add a light-theme context menu handler and a "Run theme test" button that executes runContextMenuThemeTest(), which programmatically renders menus to verify the theme attribute and .puter-theme-dark class for dark, light, and default cases (mirrors existing Playwright assertions). Update testContextMenu and testContextMenuDark to use the new item factory and clean up rendered menus after checks. UI text is updated to show pass/fail results.

---------

Co-authored-by: jelveh <[email protected]>
2026-06-24 11:30:16 -07:00
Neal Shah a22dc5facb fix whoami user token check (#3299) 2026-06-24 14:28:02 -04:00
Neal Shah aeafd4ac79 txt2img megaupdate (#3294)
* Update OpenAIImageProvider with txt2img with input source

* Grok image provider mega update

* make input_images universal
2026-06-24 10:24:09 -04:00
Daniel Salazar 14861df6b2 fix: bad boot error (#3295) 2026-06-24 00:33:38 -07:00
Daniel Salazar 34a7595a6d fix: bad empty file error handling (#3293) 2026-06-23 16:13:41 -07:00
Daniel Salazar b2108c0216 fix: update email for supprot (#3292) 2026-06-23 15:43:12 -07:00
Philip Ifeanyi 5a5a449210 feat: enhance playground UI with page title and style adjustments (#3289) 2026-06-23 22:45:18 +07:00
Daniel Salazar 0405de30bd update taskbar apps (#3286) 2026-06-22 14:26:35 -07:00
Nariman Jelveh 40c2799b06 Add 'builder' to recommended apps list
Include 'builder' in the RECOMMENDED_APP_NAMES array in src/backend/services/apps/RecommendedAppsService.ts so the RecommendedAppsService treats the builder app as a recommended application and surfaces it accordingly.
2026-06-22 14:22:27 -07:00
Daniel Salazar 4bcdeefe68 chore: bump up deps (#3285) 2026-06-22 13:58:45 -07:00
Daniel Salazar b6ba6dd90e fix: [PUT-1199] more prelude signals (#3282) 2026-06-22 08:16:59 -07:00
Daniel Salazar 954f8b7b8d fix: puter self host guides + default bug (#3281)
* fix: oss dev center html_encode hardening

* fix: puter self host guides + default bug
2026-06-21 22:32:43 -07:00
Neal Shah 24d700673c Compaction support for OpenAI and Anthropic (#3279)
* Alpha: compaction support for OpenAI and Anthropic

* update lock

* fix billing for anthropic compactions

* Fix max_tokens bug in together provider

* Fix responses compaction
2026-06-21 21:57:56 -04:00
c6d64029e0 feat: meter Gemini thinking tokens and grounding requests (#3178)
* feat: meter Gemini thinking tokens and grounding requests

- Thinking tokens: Extracted from standard completion tokens to ensure they are billed accurately at the correct model-specific rate.
- Grounding requests: Added flat-fee metering for Google Search by tracking grounding_metadata across both streaming and non-streaming responses.
- Pricing updates: Corrected stale rates for Gemini 2.5 Flash output, cached tokens, thinking tokens, and grounding requests.

* fix: correct Gemini 2.x metering rates and harden grounding capture

Pricing corrections (verified against ai.google.dev/gemini-api/docs/pricing):
- gemini-2.5-flash output is $2.50/M, not $1.00/M: restore
  completion_tokens to 250 and bill thinking_tokens at the same output
  rate (250). The previous 100 under-billed output ~60%, and this is the
  provider's default model.
- gemini-2.5-flash cache read is $0.03/M: restore cached_tokens to 3
  (the 7.5 value over-billed).
- Grounding with Google Search is $35 / 1,000 requests for Gemini 2.x
  models and $14 / 1,000 for 3.x. Set grounding_requests to 3_500_000
  for gemini-2.0-flash, gemini-2.5-flash, gemini-2.5-flash-lite and
  gemini-2.5-pro; 3.x models keep 1_400_000.

Streaming robustness:
- In create_chat_stream_handler, don't let a later extra_content chunk
  without grounding_metadata overwrite an earlier one that carried it,
  so grounding requests are still metered.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

---------

Co-authored-by: Daniel Salazar <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-20 15:08:38 -07:00
jelveh 40ff866598 Add 'butler' to recommended apps
Include 'butler' in the RECOMMENDED_APP_NAMES array in RecommendedAppsService so it appears in the recommended apps list. Updates src/backend/services/apps/RecommendedAppsService.ts.
2026-06-19 15:50:57 -07:00
Nariman Jelveh 36888e35d3 Add phone verification UI improvements and helpers (#3274)
Revamps the phone verification dialog and adds reusable phone helper modules.

- Heavily refactors UIWindowPhoneVerificationRequired.js to add a searchable inline country picker, national-number live formatting, country autodetect for "+" input, WebOTP auto-fill support, resend cooldown timer, accessibility improvements, proper teardown/cleanup, and last-used country persistence.
- Introduces helpers/src/helpers/country_codes.js (get_country_list, flag emoji) which builds a localized list of dialable countries from libphonenumber-js and Intl.DisplayNames.
- Introduces helpers/src/helpers/phone.js with thin wrappers around libphonenumber-js: format_phone_as_you_type, inspect_phone, phone_example_for, detect_country_from_input.
- Adds English i18n strings for the new UI in src/gui/src/i18n/translations/en.js.

These changes keep submission canonicalization using libphonenumber-js (E.164), improve UX for selecting/formatting numbers, and make the country list and phone utilities reusable across the app.
2026-06-19 10:28:30 -07:00
Reynaldi Chernando 30432b931a Add PUT and DELETE to preflight allowed methods (#3278) 2026-06-19 12:35:29 -04:00
Reynaldi Chernando e2174a14cf Update router cors preflight docs (#3277) 2026-06-19 20:50:41 +07:00
Daniel Salazar d3cb89d8c9 fix: import in extensions (#3276) 2026-06-18 21:30:00 -07:00
Reynaldi Chernando c8625b3396 Release puterjs npm (#3275)
* Release puterjs npm

* add package lock
2026-06-18 18:26:39 -07:00
Reynaldi Chernando b576f01d12 Sync Puter.js types and docs (#3194)
* AI chat types

* ai voice types

* update tts docs and jsdoc

* stt types and docs

* txt2img docs and types

* txt2vid and img2txt

* apps

* signin

* perms

* fs

* kv

* hosting

* workers

* net

* ui

* jsdoc
2026-06-18 09:34:13 -07:00
Vyasdev a87cafee3c Revise bug report and feature request instructions (#3220)
Updated the wording for reporting bugs and feature requests in Japanese.
2026-06-18 09:27:55 -07:00
Reynaldi Chernando 1c51fde163 Add minimax m3 direct integration + cleanup model list (#3263) 2026-06-18 10:12:44 -04:00
Daniel Salazar b05699db77 fix: AGENTS.md cleanup (#3273) 2026-06-17 17:07:20 -07:00
Daniel Salazar 163e48ab27 feat: viz over user verification types (#3271) 2026-06-17 14:21:55 -07:00
Reynaldi Chernando 0b41d992a4 Add glm 5.2 direct integration (#3269) 2026-06-17 09:40:10 -04:00
Miika Kuisma c2ddfd6867 Merge pull request #3270 from HeyPuter/fix-file-dialog
puter.js - fix issue where content of file dialog wasn't visible
2026-06-17 13:08:43 +03:00
Miika Kuisma 81b9dc1f06 fix issue where content of file dialog wasn't visible 2026-06-17 13:07:13 +03:00
Daniel Salazar 5506ddb2c2 feat: fp cleanup and better devex (#3268) 2026-06-16 22:48:54 -07:00
Neal Shah c74469b01a prelude RCS (#3267) 2026-06-15 19:08:30 -04:00
Daniel Salazar b706693f82 wip: hardening (#3266) 2026-06-15 16:07:44 -07:00
Daniel SalazarandNeal Shah cd800148d5 feat: optional additional verification gates (#3262)
* phone number verificiation initial

* finish phone verification

* feat: add card gate for signups

* chore: npm

* fix: modal order and wording

* fix:wording

---------

Co-authored-by: Neal Shah <[email protected]>
2026-06-15 09:16:00 -07:00
Ricardo Sawir 900b0d7aa6 fix: correct puter container healthcheck URL (#3255)
The healthcheck was using 'puter.localhost:4100/test' which fails for
two reasons:
- 'puter.localhost' does not resolve inside the container; the service
  listens on all interfaces at port 4100, so 'localhost' works
- '/test' endpoint does not exist; use '/' which serves the shell

This caused the container to be perpetually marked as unhealthy.

Fixes #3191
2026-06-13 23:17:44 -07:00
Daniel Salazar 7e21c1f888 fix: fs metadata sanitation (#3257) 2026-06-12 22:56:06 -07:00
Daniel Salazar 00d6f9c230 fix: disallow unverified outlook emails (#3256) 2026-06-12 18:52:24 -07:00
jelveh 4c03080efa Update mcp.md 2026-06-12 16:55:37 +02:00
jelveh 432a11d289 Add Cursor MCP docs and icons 2026-06-12 16:37:53 +02:00
jelveh e9e233404b Add icons for Claude, Codex and OpenCode 2026-06-12 13:20:15 +02:00
jelveh 8101d42604 Capitalize OpenCode in MCP docs 2026-06-12 13:16:41 +02:00
jelveh 88cedd43bc Add opencode MCP example to docs 2026-06-12 13:11:24 +02:00
Daniel Salazar d3a2934d9b feat: fingerprinting for signup checks (#3254) 2026-06-12 00:59:21 -07:00
Daniel Salazar 13a36b6199 fix: secure http calls (#3252) 2026-06-11 13:43:58 -07:00
Daniel Salazar a155464d46 fix: more data for puter site serve event (#3251) 2026-06-10 23:27:13 -07:00
Daniel Salazar 40d1c998bb feat: pass args to all events (#3248)
* feat: pass args to all events

* fix: alias app joining

* fix: actor in event
2026-06-10 14:13:04 -07:00
Daniel Salazar b4837d49b6 perf: some perf gains from fable (#3249) 2026-06-10 13:26:35 -07:00
ProgrammerIn-wonderland e23042b42e add preperation for reputation score persistence (#3250) 2026-06-10 16:09:42 -04:00
Daniel Salazar 240a733285 sec: misc fable hardening (#3244)
* sec: misc fable hardening

* more fixes

* more fixes

* fix: cors issue
2026-06-10 11:19:41 -07:00
Reynaldi Chernando bb202be12c Minor clarification local webserver in deployment (#3247) 2026-06-10 23:04:42 +07:00
Reynaldi Chernando 8c4cb601c9 Add cli docs (#3246) 2026-06-10 20:53:53 +07:00
Reynaldi Chernando 64a7c7b7c4 Add mcp docs (#3245) 2026-06-10 20:31:31 +07:00
wwb1942 2dd2887ff7 ci: add docs build workflow (#3235)
* ci: add docs build workflow

Add CI workflow that runs npm ci and npm run build in src/docs

whenever files under src/docs change in a pull request.

Closes #3225

* ci: also trigger docs build when workflow file changes

* ci: address docs build workflow review
2026-06-10 10:53:40 +07:00
ProgrammerIn-wonderland 4bbe8a2789 add protections for PUT-1094 (#3243) 2026-06-09 22:29:57 -04:00
Dilan Melvin T 3e9ceb673b fix: coerce Mistral image_url parts from object to string (#3177)
* fix: coerce Mistral image_url parts from object to string

Mistral's API expects image_url content parts to be a plain string URL,
not the OpenAI-style { url: string } object that process_input_messages
produces. Without this coercion, sending an image to any Mistral model
that supports vision (mistral-small, mistral-medium, mistral-large,
ministral-*) results in a request error from the Mistral SDK.

Add #coerceImageUrls() as a private method on MistralAIProvider that
maps { type: 'image_url', image_url: { url } } -> { type: 'image_url',
image_url: url } for every content part in every message. Messages with
plain string content are left untouched, as are parts whose image_url is
already a string.

Add four unit tests covering: object-to-string coercion, already-flat
strings, plain string message content, and mixed text+image content.

* style: use plain ASCII dashes in comment section divider
2026-06-09 20:12:44 -04:00
Daniel Salazar 9c4d1ef535 fix: misc bugs + new middleware for before/after routes (#3242) 2026-06-09 17:05:56 -07:00
ProgrammerIn-wonderland bf57518262 MCP: assert worker_exec ends with puter.work (#3241) 2026-06-09 17:43:34 -04:00
Reynaldi Chernando d48fb11de6 Add claude fable 5 (#3238) 2026-06-09 14:05:31 -04:00
ProgrammerIn-wonderland 7ed87f760b make max tokens less lenient (#3240) 2026-06-09 14:03:13 -04:00
Reynaldi Chernando ee37be16f0 Add cli deployment docs (#3239) 2026-06-10 00:40:19 +07:00
Reynaldi Chernando a8cfaeefbd Add subdomain and workers github actions deployment (#3226)
* Add subdomain and workers github actions deployment

* minor adjust
2026-06-09 23:35:34 +07:00
Badri467 58cb8ee048 skip github actions backend for docs only changes (#3236) 2026-06-09 23:28:48 +07:00
Daniel Salazar a72fbd1d47 fix: inform acl of full access flag (#3237) 2026-06-09 08:04:36 -07:00
Daniel Salazar f6408db74e fix: access tokens with full accesses allowed through with new middleware gate (#3234) 2026-06-08 18:47:07 -07:00
ProgrammerIn-wonderland df20993d8b set azure openai as the default (#3233) 2026-06-08 20:33:41 -04:00
ProgrammerIn-wonderland 708fe1bc4e azure ai provider (#3232) 2026-06-08 18:31:49 -04:00
Daniel Salazar e782fa832e fix: kv costs (#3231) 2026-06-08 13:32:36 -07:00
ProgrammerIn-wonderland c947c2c989 fix mcp server cloudfront cache bug (#3230) 2026-06-08 15:42:49 -04:00
Daniel Salazar 3de4613b7d Revert "feat (PUT-1091): have gui tokens labeled and be different than reques…" (#3229)
This reverts commit 1335b46404.
2026-06-08 11:15:24 -07:00
Daniel Salazar def7290c48 feat: emit user.deleted event on user deletion (#3228) 2026-06-08 10:30:24 -07:00
Daniel Salazar 1335b46404 feat (PUT-1091): have gui tokens labeled and be different than requestor ones (#3222)
* feat (PUT-1091): have gui tokens labeled and be different than creator ones

* fix: errors with auth token
2026-06-08 09:03:02 -07:00
Reynaldi Chernando 0e15ac98a9 Update cli readme and set version (#3227) 2026-06-08 22:23:32 +07:00
Reynaldi ChernandoandCopilot Autofix powered by AI f127b2509d Add docs fixes for deployment and workers pages (#3223)
* Add docs fixes for deployment and workers pages

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* handle router edge case

* explicit info about wildcard requiring name

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-06-08 17:43:00 +07:00
Sourabh Sharma 15134c99bf Fix mkdir dedupeName for existing directories (#3215) 2026-06-07 12:49:00 -07:00
Daniel Salazar 8262e3193e perf: don't batch get app stats (#3219) 2026-06-06 18:28:59 -07:00
Reynaldi Chernando 98a21c4ac0 More workers docs improvement (#3218)
* More workers docs improvement

* Add mention of workers alongside mention of user scope
2026-06-06 01:27:24 +07:00
ProgrammerIn-wonderland c52eb1b903 Ns/bug fixes 060426 (#3214)
* Harden backend auth

* Puter.js auth hardening
2026-06-05 13:55:23 -04:00
mohit_parmar_007 d6a71e50bb fix: remove unnecessary cache console.log statements (#3216) 2026-06-05 10:01:59 -07:00
Reynaldi Chernando a4b6ee0dd2 [PUT-1090] Improve worker docs (#3217)
* Improve worker docs

* refine
2026-06-05 21:54:56 +07:00
Daniel Salazar bb61103080 feat: add apps api into mcp (#3213) 2026-06-04 17:19:53 -07:00
Daniel Salazar 9bbc76b2dd drop metering alerts to low severity (#3212) 2026-06-04 14:01:44 -07:00
Daniel Salazar 918aba9c80 fix: reduce alarms further (#3211)
* feat; update bug-bounty

* fix: reduce alarms further
2026-06-04 10:43:50 -07:00
Reynaldi ChernandoandCopilot Autofix powered by AI 3ceba9e3b3 Minor fix deployment docs (#3210)
* Minor fix deployment docs

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-06-04 23:48:25 +07:00
Daniel Salazar a480679a2d vis: make metering service alarm only go off on actual abuse (#3208) 2026-06-03 22:45:18 -07:00
Daniel Salazar e99ae40e93 chore: bump puter-js (#3207) 2026-06-03 18:59:07 -07:00
Daniel Salazar 3cd50fbaf3 move the token migration to root (#3206) 2026-06-03 18:54:22 -07:00
ProgrammerIn-wonderland 60dd1453d4 Fix issues (#3205) 2026-06-03 21:19:33 -04:00
ProgrammerIn-wonderland 07781cd681 allow overriding origin from workers.dev on mcp server (#3204) 2026-06-03 17:48:37 -04:00
ProgrammerIn-wonderland 2387d8dcc9 MCP whoami (#3203) 2026-06-03 17:13:13 -04:00
Reynaldi Chernando 723608f4db Add new puter cli (#3199)
* Add new cli

* update package lock

* fix readme and license
2026-06-04 01:51:31 +07:00
Reynaldi Chernando bd1dcde7b0 Add info for puterjs deployments (#3202)
* Add info for puterjs deployments

* update external links

* hide github actions method
2026-06-04 01:25:57 +07:00
ProgrammerIn-wonderland 5b8e41e66d MCP Server (#3197)
* beginnings of mcp

* oauth support

* add workers and better prompt engineering
2026-06-03 12:23:06 -04:00
Nariman Jelveh acc4796b99 Allow Cache-Control and Pragma headers (#3198) 2026-06-03 14:30:33 +02:00
jelveh 106e85d6ca Force revalidation on FileSystem reads 2026-06-03 14:15:22 +02:00
ProgrammerIn-wonderland 0ffc32f19e add whitelist for signed batch uploads (#3196) 2026-06-02 21:30:34 -04:00
Daniel Salazar d897cc5dd0 refactor: move subscriptions to webhook system (#3193)
* fix: dont sign write urls if they don't have write perm

* wip: new subscriptions

* refactor: move subscriptions to
2026-06-02 17:58:56 -07:00
Daniel Salazar 6d2f277ce2 fix: some error handling (#3190) 2026-05-31 18:58:27 -07:00
ProgrammerIn-wonderland 86f80b29c0 tighten RAO policy (#3188) 2026-05-29 20:52:21 -04:00
Daniel SalazarandProgrammerIn-wonderland 39243735e8 feat (put-1049): worker types (#3185)
* feat (put-1049): worket types

* Implement changes to worker types

---------

Co-authored-by: ProgrammerIn-wonderland <[email protected]>
2026-05-29 20:51:57 -04:00
ProgrammerIn-wonderland b3ff9d74b7 add browserjs to taskbar and RecommendedAppService (#3189) 2026-05-29 20:32:53 -04:00
ProgrammerIn-wonderland 7b7604440f Add serverless only filtering for togetherai chat models (#3187) 2026-05-29 14:33:55 -04:00
ProgrammerIn-wonderland 382d2b3c14 add minimax provider (#3171)
* add minimax provider
2026-05-29 13:36:10 -04:00
Anurag Pappula eda5379c17 fix(docs/search): handle search index loading and failure states (#3184)
* fix(docs/search): show loading state and replay query when index isn't ready

Previously performSearch rendered "No results found" while the
MiniSearch index was still loading, which is misleading on slow
networks. Track the last query as pendingQuery and replay it from
fetchSearchIndex once the index is ready; render a distinct
"Failed to load search index" message on fetch error so the
loading message doesn't persist indefinitely.

Fixes #3180.

* fix(docs/search): close stale-replay race and consolidate status messages

Drop pendingQuery synchronously in the input handler when the query
falls below the minimum length, so a fetchSearchIndex that resolves
within the 300ms debounce window after the user clears the field can
no longer replay the cleared query into an empty input.

Move the idle, loading, and failed copy into a single
renderSearchStatus(kind) helper so the four .search-no-results
renderings can only drift in one place.
2026-05-29 16:00:59 +07:00
Ron HernausandDaniel Salazar d2fee51844 Support PostgreSQL database backend (#3167)
* feat(database): add postgres database client

Adds the PostgreSQL database client, native bootstrap migration, SQL preparation helpers, and database config/factory wiring.\n\nRefs #3165.

* feat(database): make backend queries postgres-aware

Updates runtime SQL call sites for database-specific booleans, identifiers, insert-ignore, upserts, JSON extraction, intervals, and Postgres insert ids.\n\nRefs #3165.

* test(database): cover postgres client behavior

Adds unit coverage for SQL preparation, factory selection, write-result mapping, and transaction rollback/commit ordering, plus an env-gated PostgreSQL integration flow.\n\nRefs #3165.

* docs(self-hosting): document postgres database setup

Adds PostgreSQL configuration examples and migration path guidance for self-hosted deployments.\n\nRefs #3165.

* fix: harden postgres oidc tests

* fix(postgres): normalize query results and SQL prep

* fix(user): preserve normalized cache booleans

* test(postgres): run integration coverage with pgmock

* tests: add way to run all tests with postgres though slow

Also adding note that postgres is not in active use so might not work out the box

---------

Co-authored-by: Daniel Salazar <[email protected]>
2026-05-28 14:12:17 -07:00
Daniel Salazar 8ac4bad196 fix: readUrl perm check (#3183) 2026-05-28 11:44:16 -07:00
Reynaldi Chernando f140d7221c Add opus 4.8 (#3182)
* Add opus 4.8

* update alias

* handling opus 48 omit temperature
2026-05-28 13:27:46 -04:00
Daniel Salazar 631f0bda1b chore:deploy latest puter.js (#3181) 2026-05-28 09:58:33 -07:00
Reynaldi Chernando 1121314582 Docs search improvement (#3179)
* Init search improvement

* Add fuzzy implementation

* update package lock

* package lock matched with runner 24.16.0
2026-05-28 23:11:34 +07:00
Daniel Salazar dda6b82f0f fix: missing english i18n (#3176) 2026-05-27 16:31:40 -07:00
ProgrammerIn-wonderland 6c757c3fc8 Don't use passed in authorization in workers (#3175) 2026-05-27 18:09:59 -04:00
Daniel Salazar eba194936e tests: more tests for session and auth (#3174) 2026-05-27 12:32:15 -07:00
Daniel Salazar a7bdac16da tests: Add unit tests for XAISpeechToTextDriver (#3173)
closes #3002
2026-05-27 11:49:16 -07:00
Daniel Salazar 716a8d06e2 fix: session store kind column + duplicate error handling (#3172) 2026-05-27 11:44:08 -07:00
Daniel Salazar e95cf44fec fix: small fixes for perf and username checks (#3169) 2026-05-27 03:10:56 -07:00
Daniel Salazar 8f8efcb349 feat (PUT-1025 PUT-1026 and PUT-1017): better handling old token auth required in gui (#3166)
* feat (PUT-1025 PUT-1026 and PUT-1017): better handling old token forcing in gui

* fix: misc issues with token inval project
2026-05-27 02:26:35 -07:00
Daniel Salazar b188942436 feat (PUT-1016 & PUT-1020) (#3164)
* feat (PUT-1016 & PUT-1020)
temp account preservation on forced relogin
hosted asset cookies to v2 token too

* fix: remove llm dashes and ugly comments

* update agents
2026-05-26 23:35:16 -07:00
Daniel Salazar 9b7e9a98c8 fix: session revocation (#3163) 2026-05-26 22:13:12 -07:00
Daniel Salazar d6f7fb6155 chore: package-lock update (#3161) 2026-05-26 20:33:11 -07:00
Daniel Salazar bd91f5e192 feat: worker sessions get their own kind + per-(user, app, worker_name) row (#3160)
* feat: worker sessions get their own kind + per-(user, app, worker_name) row

Schema
------
- mysql_mig_11.sql + sqlite 0054: add idx_sessions_user_worker_active,
  a partial unique index over (user_id, app_uid, meta.worker_name) for
  kind='worker' rows. Active worker sessions are deduped by that triple
  so each named worker gets its own session row and they don't fight
  the existing idx_sessions_user_app_active (which still constrains
  kind='app' only). app_uid is allowed NULL for user-scoped workers
  with no app binding.

SessionStore
------------
- getOrCreateWorker(userId, { appUid, workerName, ... }): mirrors the
  getOrCreateApp pattern — cache lookup, partial-unique re-SELECT on
  insert-ignore, all keyed on the worker triple. expires_at lands at
  WORKER_WINDOW_SECONDS (~99y) so the worker doesn't have to re-mint
  on any cadence.
- #cacheKeyWorker + #allCacheKeysForRow worker branch so revoke /
  update invalidates the worker cache view alongside the by-uuid one.

AuthService
-----------
- createWorkerSessionToken(user, workerName, meta?) now takes the
  workerName explicitly and routes through getOrCreateWorker. Emits
  the same { session, token, gui_token } shape but both JWTs carry
  { worker: true, worker_name }.
- createWorkerAppToken(actor, appUid, workerName) likewise — JWT
  carries the worker_name claim so a verifier can tell two workers
  under the same app apart without a DB round-trip.
- Both methods 400 on empty workerName.

WorkerDriver
------------
- Five auth-mint call sites swapped over: app-bound deploy (3x:
  appId branch, actor.app fallback, hot-reload redeploy), user-bound
  fallback (2x: cold deploy, hot-reload). All pass `workerName` so
  the worker's session row is naturally idempotent across redeploys.

GUI manage-sessions
-------------------
- sessionTitle adds a kind='worker' branch ("name (app)" for
  app-scoped workers, just "name" for user-scoped), pulling worker_name
  from the meta-spread that listSessions already surfaces.
- en.js adds ui_session_kind_worker.

* fix(workers): MySQL JSON_EXTRACT quoting + revoke cache invalidation +
SQLite NULL-distinct in worker index

Three real bugs in the worker session plumbing from the prior commit,
plus a misleading comment. Schema design kept (worker_name lives in
`meta` rather than a dedicated column) per offline review:

1. `#selectWorkerRow` compared `JSON_EXTRACT(meta, '$.worker_name')`
   directly to a bind parameter. MySQL's `JSON_EXTRACT` returns a
   JSON-typed value with embedded quotes (`"name"`, not `name`), so
   the comparison never matched. After the first INSERT, every
   follow-up getOrCreateWorker call missed the existing row in the
   SELECT, hit INSERT-IGNORE, then missed again in the re-SELECT —
   the caller would receive whatever the INSERT-IGNORE returned (a
   no-op row in conflict cases). Wrap with `JSON_UNQUOTE` on MySQL
   via `db.case`; SQLite's `json_extract` already returns the
   unwrapped scalar so it keeps the literal form.

2. mig_11's generated `worker_unique_key` had the same JSON-quoting
   bug. Mirror the fix: `IFNULL(JSON_UNQUOTE(JSON_EXTRACT(...)), '')`
   so the concatenated unique key is a plain string that lines up
   with what `#selectWorkerRow` now binds against.

3. SQLite UNIQUE indexes treat NULL columns as distinct (per the SQL
   standard), so two user-scoped workers (app_uid NULL) with the same
   worker_name would both insert. Wrap the index expression with
   `IFNULL(app_uid, '')` so they correctly conflict — matches the
   MySQL side's `IFNULL` in the generated column.

4. `removeByUuid` / `revokeCascade` SELECTed only the identity
   columns (no `meta`), so `#allCacheKeysForRow`'s worker branch
   couldn't read `meta.worker_name` and the composite
   `sessions:v2:worker:<user>:<app>:<name>` cache key survived
   revocation. Up to CACHE_TTL_SECONDS (15min) afterwards,
   getOrCreateWorker would short-circuit to the cached (revoked) row.
   Add `meta` to both SELECTs; the existing meta-parsing logic in
   `#allCacheKeysForRow` handles the rest.
2026-05-26 20:05:31 -07:00
Daniel SalazarandClaude Opus 4.7 971a1b5071 chore: drop linear/issue ticket references from source comments (#3159)
54 references across 18 files (PUT-1010, PUT-1014, PUT-1019, PUT-1021,
PUT-1022, PUT-1023, PUT-1024 + sub-tags AUTH-2/4/5, SDK-1, PJS-1/2,
GUI-1/2, ROLLOUT-1) removed from inline comments, doc comments,
test describe blocks, and SQL migration headers. The substantive
explanations stay; only the ticket pointers go.

No behavior change. Full backend test suite: 2172 passed / 16 skipped.

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-26 18:50:43 -07:00
Daniel SalazarandClaude Opus 4.7 ac5eecb7f3 feat (put-1019 put-1021): v2 auth revoke endpoints + silent v1->v2 to… (#3158)
* feat (put-1019 put-1021): v2 auth revoke endpoints + silent v1->v2 token migration

PUT-1019 (AUTH-5): full revoke-endpoint coverage
- /logout: soft-revoke web session + its asset cookies via revokeCascade
  (app sessions and access tokens survive)
- POST /auth/revoke-session: cascade per row kind (web/app/access_token/asset)
- POST /auth/revoke-all-sessions: revoke all web rows for user; optional
  include_apps=true nuclear option; gated by userProtected (cookie-only)
- revokeAccessToken: soft-revoke matching access_token row in addition to
  removing access_token_permissions
- All revokes are UPDATE revoked_at = now(); no DELETE statements remain

PUT-1021 (SDK-1): backend POST /auth/migrate-token
- v1 access_token/app -> mint matching-kind v2 token, idempotent on
  (auth_id, kind, token_uid)
- v1 web/session -> 409 { code: "reauth_required" } (interactive relogin only)
- Same-origin / signed-referer hardening; rate-limited per IP and auth_id
- Gated by auth.allow_v1_tokens; emits puter_token_v2 cookie for app-in-browser

DB migrations: mysql_mig_10, sqlite 0053 (sessions.access_token_uid column)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(put-1019): reject self-revoke; enrich list-sessions response

- handleRevokeSession refuses uuid === req.actor.session.uid (use /logout
  instead). The cookie that authenticated the call should never be the
  target of a self-revoke — the response can't write fresh auth state
  and the client ends up with an ambiguous identity. revoke-all-sessions
  still has the explicit include_current opt-in for the nuclear case.

- AuthService.listSessions now joins the apps table for kind='app' rows
  (returning { uid, name, title, icon } so the manage-sessions UI can
  render the authorizing app without a second round trip), surfaces
  kind / expires_at / label / last_ip / created_via, and filters out
  asset rows (per-cookie children of web rows, revoked transitively via
  cascade — surfacing them as standalone entries would be confusing).

- Sort order: current session first, then most-recently-active. UI
  relies on this to anchor "you are here" at the top.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(put-1019 put-1021): review nits — origin normalization, cookie fallback, types, migration order

B1: createAccessToken.options.expiresIn widened to string | number.
    The impl (#hardExpiryFromExpiresIn) and existing callers/tests use
    jsonwebtoken-style strings ('1h', '30d'); narrowing to number forced
    unsafe casts at every call site. Cast at the single sign() boundary
    where jsonwebtoken's typed template-literal SignOptions clashes
    with the wider runtime contract.

B2: Inline comment on the DELETE in access_token_permissions. The
    AUTH-5 "no DELETE on revoke" rule scoped to the `sessions` table
    (where the cascade graph + audit trail matter). Permissions rows
    are the grant manifest for an active token — once its session is
    soft-revoked they're dead-weight cache entries. A future audit
    requirement would land as a `revoked_at` column on this table,
    not a behavior change in this PR.

B3: handleMigrateToken now sets the puter_token_v2 cookie (with the
    shared sessionCookieFlags + httpOnly) when the migration result
    is kind='app'. The endpoint is already gated on Origin so the
    caller is by definition in a browser; access tokens deliberately
    skip the cookie since they're programmatic.

B4: #isMigrateTokenOriginAllowed normalizes both incoming origin and
    config.origin / allowlist entries (trim + strip trailing slash +
    lowercase) before equality. A misconfigured `config.origin =
    "https://puter.com/"` would otherwise reject every same-origin
    browser call.

B5: Replaced 4x `this.config.cookie_name!` non-null assertions in
    AuthController with `(this.config.cookie_name ?? 'puter_token')`.
    IConfig is `Partial<IConfigOptional>` so cookie_name is undefined
    at runtime in some deployments / test setups; the fallback matches
    the pattern in userProtected / OIDCController / puterSite.

B6: MySQLDatabaseClient sorts migrations numerically by trailing
    integer instead of lexically. Existing files use unpadded names
    (`mysql_mig_<N>.sql`), so plain `.sort()` ran mysql_mig_10 before
    mysql_mig_2 — a future migration that depended on _2..9 running
    first would break. Non-numeric filenames fall through to
    localeCompare for determinism.

B7: Restored the docstring for SessionStore.getOrCreateApp's
    `opts.auth_id` ("Stable per-user identity (survives re-login);
    carried on every v2 JWT so manage-sessions can group by identity")
    — the previous edit truncated it to a fragment.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* test + feat: backend test coverage for PUT-1019/1021 review fixes + worker session methods

Tests
-----
- compareMigrationFilenames (new) covering B6 numeric-sort: confirms
  mysql_mig_10.sql lands after mysql_mig_9.sql; non-numeric files sort
  after numbered ones; stable for already-ordered input.
- listSessions (AuthService.test.ts): excludes kind="asset" rows;
  enriches with kind/expires_at/last_ip/created_via; joins kind="app"
  rows with the apps table; sorts current first then by last_activity
  desc.
- handleRevokeSession (AuthController.test.ts): refuses self-revoke
  with 400; still allows revoking a sibling session.
- handleMigrateToken (AuthController.test.ts): rejects missing/
  disallowed Origin; tolerates trailing slash and uppercase Origin
  (B4 normalization); returns 409 reauth_required for v1 web tokens;
  does NOT set the cookie for access-token migration; DOES set the
  puter_token_v2 cookie (httpOnly + sessionCookieFlags) for
  app-under-user migration.
- SessionStore tests updated to import APP_WINDOW_SECONDS /
  WEB_WINDOW_SECONDS rather than hardcoded 30/90 day values — the
  windows just got bumped to 1y and the assertions need to follow
  the constant.

Refactor
--------
- MySQLDatabaseClient exports compareMigrationFilenames so the sort
  logic is unit-testable in isolation.

Worker tokens
-------------
- AuthService.createWorkerSessionToken(user, meta?): mints a new
  kind="web" row tagged meta.worker=true, expires_at =
  WORKER_WINDOW_SECONDS, returns { session, token, gui_token }
  with worker: true on each JWT.
- AuthService.createWorkerAppToken(actor, appUid): mints a new
  kind="app" row tagged meta.worker=true, expires_at =
  WORKER_WINDOW_SECONDS, returns an app-under-user JWT with
  worker: true. Note the existing idx_sessions_user_app_active
  unique index will collide with an existing non-worker app
  session for the same (user, app) — future schema work can
  carve workers out of that uniqueness.

SessionStore.js: WEB/APP_WINDOW_SECONDS now 1y;
WORKER_WINDOW_SECONDS = 99y added for the worker path.

Full backend suite: 2172 passed / 16 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-26 18:49:36 -07:00
Daniel SalazarandClaude Opus 4.7 3ae076b73e feat (put-1022 put-1023 put-1024): puter.js reauth handling + GUI v2 modal + silent v1 migration (#3157)
* feat (put-1022 put-1023 put-1024): puter.js reauth handling + GUI v2 modal + silent v1 migration

PUT-1022 (PJS-1): puter.js handles 401 reauth_required
- Single apiCall wrapper intercepts 401 { code: "reauth_required" }
- Clears local token, emits puter.auth.reauth_required { reason, auth_id }
- Queues in-flight requests, re-issues after re-auth completes
- web/app: opens puter.com login popup forwarding auth_id
- gui: no-op (handled by GUI-1 modal)
- workers: surfaces as structured exception
- All token writes go through setAuthToken() (future-proofed for PJS-2)

PUT-1023 (GUI-1): web GUI reauth modal + v2 token storage
- Detects 401 reauth_required across http + websocket connect paths
- Soft modal preserves URL/window state; auth_id forwarded into login form
- Storage key migrated: auth_token -> auth_token_v2
- Cookie cleared on logout: puter_token (legacy) cleared, backend writes puter_token_v2
- Cross-tab propagation via localStorage storage events
- WebSocket revocation surfaces as same modal (no silent failure)

PUT-1024 (PJS-2): puter.js storage versioning + silent v1->v2 migration
- New storage key puter.auth.token.v2
- On SDK init: prefer v2 key; else legacy v1 -> silent POST /auth/migrate-token (SDK-1)
- On success: store v2, clear v1. On failure: fall back to PJS-1 reauth flow
- URL-param tokens (?puter.auth.token=, ?auth_token=) also run through silent migration
- Logout clears both keys; setAuthToken writes v2 only

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(put-1023): manage-sessions polish — hide self-revoke, fix modal stack, richer rows

Three issues surfaced during local testing of the manage-sessions UI:

1. The session row representing the caller's own cookie had a Revoke
   button that, if used, left the client in an ambiguous identity state
   (backend now rejects it too — see put-1019 backend fix). The button
   is now omitted entirely when session.current=true; /logout remains
   the right path for ending the active session.

2. The confirm-revoke prompt rendered behind the manage-sessions
   window because both share the dominant z-index pool. UIAlert calls
   now pass parent_uuid (the manage-sessions window's data-element_uuid)
   plus stay_on_top, so the prompt stacks above its parent.

3. Each row only showed the bare uuid. Now renders: title (app
   title for app sessions, label / "Browser session" / "Access token"
   otherwise), app icon when applicable, kind / current badges,
   created / last-active / expires (timeago, with absolute on hover),
   and last_ip. App metadata is joined server-side per the backend
   listSessions change.

Adds en.js strings for the new labels (ui_session_*).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(put-1022): tighten reauth replay + iframe postMessage hygiene

driverCall_ replay (G1): on 401 reauth_required, re-enter driverCall_
instead of calling replayXhrAfterReauth. The generic helper wires the
retried XHR through setupXhrEventHandlers which resolves with the
parsed response and silently skips driverCall_'s streaming detection,
usage-limit / email-confirmation handling, settings.transform, and
resp.result unwrapping — i.e. it would change the driver call API
contract on retry. One-shot via settings._reauthReplayed so a
fresh-token rejection bubbles up instead of looping.

responseType drift (G2): driverCall_ sets xhr.responseType from
settings AFTER initXhr (which captured the stale value into
xhr._puterReq). Mirror the mutation onto _puterReq so any replay path
builds the retry with the live config rather than the snapshot.

targetOrigin lockdown (G3): triggerReauth's parent.postMessage now
targets this.defaultGUIOrigin instead of '*'. The payload carries
reauth metadata + auth_id and is only meaningful to the GUI; '*'
would leak the signal to whatever frame happened to be embedding us.

event.source pinning (G4): the reauth wait listener also matches
event.source against globalThis.parent, not just event.origin —
origin alone admits any same-origin frame on the GUI domain.

event name alignment (G5): the SDK now emits and listens for
'puter.auth.reauth_required' (matches the documented name in the
PR / commit description). The old 'auth.reauth_required' key isn't
yet consumed externally so this is a safe rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-26 18:49:17 -07:00
Daniel Salazar 5394ccc45c feat: add verification for v2 auth (#3155) 2026-05-26 15:34:54 -07:00
Daniel Salazar 3649109f40 fix: add back AGENTS.md (#3154) 2026-05-25 16:01:35 -07:00
Daniel Salazar 0ff0ea7743 fix: fs event emissions (#3153) 2026-05-25 14:25:27 -07:00
Daniel Salazar 8be115096f feat (put-1012 put-1014): new tokens version signature and logic (#3152) 2026-05-25 14:19:23 -07:00
Daniel Salazar e66fd2373f feat: start adding support for token expiry and invalidation (#3151)
* feat: new expirable + revokable session

* fix: msql migration
2026-05-25 12:30:20 -07:00
jelveh 0e2cce152d Update PuterAlert.js 2026-05-24 17:03:34 -07:00
Daniel Salazar 101e0bc930 fix: search leaking metadata (#3150) 2026-05-24 16:21:33 -07:00
jelveh ea078250bf Strip non-essential media from docs' markdown 2026-05-24 16:15:24 -07:00
jelveh db79f8e484 Add note: serve Puter.js via HTTP server 2026-05-24 15:48:21 -07:00
Daniel Salazar 6422f2d513 fix: associated app ids as input (#3149) 2026-05-23 13:01:03 -07:00
ProgrammerIn-wonderland c60626f772 Add preamble versioning (#3147)
* Add preamble versioning

* update migrations index
2026-05-23 01:15:58 -04:00
ProgrammerIn-wonderland cafdaf7b50 fix bug with signed url directory listings (#3146) 2026-05-22 18:48:41 -04:00
Daniel Salazar bb0f56d758 fix: bring back puter site config logic (#3145) 2026-05-22 11:15:45 -07:00
NighttandReynaldi Chernando 7438db7a90 Add OpenRouter model release dates (#3144)
* Add OpenRouter model release dates

* simplify implementation

---------

Co-authored-by: Reynaldi Chernando <[email protected]>
2026-05-22 12:58:59 -04:00
Daniel Salazar 948f771838 feat: ai limits tightning and add severity alert (#3142) 2026-05-21 20:46:19 -07:00
Nariman Jelveh 82e34f014d Warn on file:// loads using PuterDialog 2026-05-21 19:15:21 -07:00
Nariman Jelveh 9e4c3f8ba3 Merge branch 'main' of https://github.com/HeyPuter/puter 2026-05-21 14:39:44 -07:00
Nariman JelvehandClaude Opus 4.7 e6acd7386a Bump @heyputer/puter.js to 2.4.2
Record the 2.4.2 release in the repo. The version was published to npm
(via prepublishOnly's `npm version patch`) but the bump was never
committed, leaving package.json stuck at 2.4.1 on GitHub.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-21 14:39:18 -07:00
Daniel Salazar b04e8b046a fix: fixing misc issues around alerting and bugs (#3141)
- decrease ai related alerts and make errors not 500
- decreased bad fs controller checks to be 404s instead of 500s
- force bucket regions for now
- validate webdav perms for locking/unlocking
2026-05-21 14:26:47 -07:00
Nariman Jelveh 3262b984b7 Unify Email/Usage dialogs with Puter dialog style 2026-05-21 14:26:26 -07:00
Nariman Jelveh 71af88c73c Bump @heyputer/puter.js to 2.4.1 2026-05-21 14:06:48 -07:00
Nariman Jelveh 2275f65169 Centralize auth popup handling and consent dialog 2026-05-21 12:06:47 -07:00
ProgrammerIn-wonderland 71b7d6367f fix together provider issues (#3140) 2026-05-21 08:58:48 -04:00
Nariman Jelveh 08369d840b Add cssstyle to docs dependencies (#3139) 2026-05-20 17:41:02 -07:00
Daniel Salazar 94236133b5 fix: event types (#3138)
* fix: agents.md

* fix weird names

* fix: weird names again

* fix: event types

* cleanup types
2026-05-20 16:26:16 -07:00
Daniel Salazar 84b48541bd fix: event types + emit subdomain serve event (#3137)
* fix: agents.md

* fix weird names

* fix: weird names again
2026-05-20 12:47:56 -07:00
Daniel Salazar a33d02f522 fix: agents.md name (#3136) 2026-05-20 12:18:35 -07:00
Reynaldi Chernando f94d4aa7d9 Update gemini 3.5 flash release date (#3135) 2026-05-20 13:25:46 -04:00
Daniel Salazar 89cfb0c634 fix: subdomain htmls (#3134) 2026-05-19 22:04:57 -07:00
ProgrammerIn-wonderland c9096e9eed more validation for signed URLs (#3133) 2026-05-19 19:04:42 -04:00
Daniel Salazar 859ca207c5 feat: new gemini 3.5 flash model (#3131) 2026-05-19 15:16:07 -07:00
Daniel Salazar 58fc122f0d fix: app creation dupped names limit (#3130)
* fix: app creation dupped names limit

* fix: app dupped names
2026-05-19 12:18:59 -07:00
ProgrammerIn-wonderland 0e990da961 import ESM by URL instead of path (#3129) 2026-05-18 19:31:03 -04:00
db8236cc97 update Indonesian section (Indonesian language) (#3117)
* Update id.js

* Update id.js

* Update id.js

* Update id.js

* Update README.id.md

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <[email protected]>

* refine indonesia i18n

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Reynaldi Chernando <[email protected]>
2026-05-18 17:15:01 +07:00
jelveh d731ccf873 Bump puter.js version to 2.3.4 2026-05-17 23:20:41 -07:00
Daniel Salazar 6493d66901 fix: misc write issues with fs + openrouter refresh (#3125)
* feat: refresh openrouter models every 15 min

* fix: if no extensions, skip build step

* fix: misc write issues with fs
2026-05-17 12:34:08 -07:00
Daniel Salazar 9e7f38f473 fix: validate private apps token too (#3124) 2026-05-17 12:28:32 -07:00
Daniel Salazar a568ca0f99 fix: expose upstream ai errors as not 500s (#3123) 2026-05-17 12:01:22 -07:00
Nariman Jelveh 41e7228a9f Update PuterDialog.js 2026-05-16 17:01:40 -07:00
Nariman Jelveh 4568639718 Adjust PuterDialog footer and button spacing 2026-05-16 16:57:35 -07:00
Nariman Jelveh 73ecff0bd7 Tweak PuterDialog spacing and button widths 2026-05-16 16:50:31 -07:00
Nariman Jelveh f2f023880f Remove mobile column-reverse for dialog buttons 2026-05-16 16:42:27 -07:00
Nariman Jelveh f2093e9204 PuterDialog: UI/style tweaks and button reorder 2026-05-16 16:36:53 -07:00
Nariman Jelveh 23497dd371 Add dark mode for puter.js login prompt 2026-05-16 16:04:29 -07:00
Nariman Jelveh 37619fb2a3 Remove embedded background image from PuterDialog 2026-05-16 15:43:15 -07:00
Nariman Jelveh f976686eba Update index.md 2026-05-16 13:12:31 -07:00
ProgrammerIn-wonderland 7af3721543 Make confirming an email unconfirm any other accounts using that email (#3121) 2026-05-16 15:07:38 -04:00
ProgrammerIn-wonderland 93edec0d01 assert normalized (#3119) 2026-05-15 17:01:32 -04:00
Daniel Salazar 54b61f2e24 fix: bad cookie parse test (#3120)
* fix: deepseek alias

* fix: bad cookie parse test
2026-05-15 13:40:43 -07:00
Nariman Jelveh 184ed77493 Use fixed colors for OIDC login divider 2026-05-14 20:26:46 -07:00
Nariman Jelveh 8234b72fc3 Improve login UI: logo, forgot link, OIDC divider 2026-05-14 20:20:21 -07:00
Nariman Jelveh cc9921945e Make auth logo clickable for cover pages 2026-05-14 20:13:33 -07:00
Daniel Salazar ac6f23a41f Update README.md (#3116) 2026-05-14 13:01:34 -07:00
Nariman Jelveh 973d046d33 Remove legacy TabFiles component
Delete the large TabFiles.js file (file browser tab) and update dashboard, styles, helpers, and init logic accordingly. Related changes in UIDashboard.js, dashboard.css, style.css, helpers.js and initgui.js remove or adapt references to the removed component and adjust UI/initialization and styling to account for the file-tab extraction. This reduces duplicate/legacy code and centralizes file browser functionality; review UIDashboard and helper updates to ensure file management features (navigation, upload, context menus, previews) are migrated or re-implemented as needed.
2026-05-14 12:13:48 -07:00
Miika Kuisma dcb50cde8f Merge pull request #3115 from HeyPuter/puter-js-update260514-2
PuterJS - Fix submenu behavior on mobile devices in Menubar web component
2026-05-14 11:45:47 +03:00
Miika Kuisma 08beb55853 PuterJS - Fix submenu behavior on mobile devices in Menubar web component 2026-05-14 11:43:47 +03:00
Miika Kuisma bb0220e163 Merge pull request #3114 from HeyPuter/puter-js-update-260514
Puter JS MenuBar Web Component improvements
2026-05-14 09:14:19 +03:00
Miika Kuisma f3e4c8c6d0 menu item focus tweak 2026-05-14 09:09:40 +03:00
Miika Kuisma 44254671bb - When navigating with keyboard the main level of menubar (file, edit etc) is should also open the menus, now it just highlights the opener
- When hovering main menu items, it must clear hover effects
- Two submenus under each other and diagonal moving
- separator hovering removes highlight but disabled item should do the same
- better way to remove focus
- when mouse leaves context menu highlight should be removed
2026-05-14 08:26:36 +03:00
Daniel Salazar bcc6cc8f00 fix: express proxy chain (#3112) 2026-05-13 18:13:39 -07:00
jelveh 84549a1c31 Add Authorization header to deletion request 2026-05-13 17:21:40 -07:00
Daniel Salazar 0276420213 fix: treat access tokens as non-auth unless opted in (#3111)
* fix: treat access tokens as non-auth unless opted in

* fix: tests
2026-05-13 14:20:15 -07:00
ProgrammerIn-wonderland 6353a08fd5 accept web input in speech2txt (#3110) 2026-05-13 15:53:16 -04:00
Daniel Salazar c265fa308a fix: double rate limits across the board (#3108)
* fix: sqlite auto create path if missing

* fix: double rate limits across the board
2026-05-13 09:30:08 -07:00
Miika Kuisma d3f72bc33a Merge pull request #3105 from HeyPuter/puter-js-menubar-fixes
Puter JS UI Component updates
2026-05-13 11:51:42 +03:00
ProgrammerIn-wonderland 700fc4d27e alibaba provider (#3103)
* Add alibaba provider

* add tests
2026-05-13 03:21:15 -04:00
Miika Kuisma 9c7f30a150 Add theme attribute for each web component, which can be used for forcing dark or light theme (in case the application doesn't have dark mode support it might be odd if just menubar turns dark) 2026-05-13 10:08:59 +03:00
Miika Kuisma 85e45b30f5 fixes https://github.com/HeyPuter/puter/issues/3102 2026-05-13 09:52:21 +03:00
Miika Kuisma 5483e32eab fixes https://github.com/HeyPuter/puter/issues/3101 2026-05-13 09:37:36 +03:00
Miika Kuisma f63c7d8142 - Fixes https://github.com/HeyPuter/puter/issues/3100
- Also fixes similar issues with navigating top level menubar items and submenus
2026-05-13 09:27:44 +03:00
Miika Kuisma 5a2071f852 fix https://github.com/HeyPuter/puter/issues/3099 2026-05-13 09:01:57 +03:00
Miika Kuisma 238ed12e28 - add dark mode support for UI components
- fix https://github.com/HeyPuter/puter/issues/3098
2026-05-13 08:54:23 +03:00
Daniel Salazar d1afe6c3cf fix: puter site redirect for non paid apps (#3104)
* fix: puter site redirect for non paid apps

* add tests
2026-05-12 21:03:33 -07:00
Nariman Jelveh e58dd48e80 Fix link formatting in BUG-BOUNTY.md (#3097) 2026-05-12 17:19:10 -07:00
Daniel Salazar 69f87d4dcd fix: return code (#3096)
* fix: return code

* fix: http error
2026-05-12 16:54:00 -07:00
Daniel Salazar 7b0dd11f7f fix: dev accounts not showing (#3095) 2026-05-12 16:39:42 -07:00
Daniel Salazar 620934fbf4 fix: add user_agent to email signup (#3094) 2026-05-12 15:51:30 -07:00
Daniel Salazar 182fc723f1 expose hasDevAccountAccess at top level (#3093) 2026-05-12 13:56:00 -07:00
Daniel Salazar 5d3c5b6873 fix: whoami (#3092) 2026-05-12 13:44:02 -07:00
slashdevcorpseandCodex f20445bab7 Restrict cross-origin session-cookie auth (#3088)
* Restrict cross-origin cookie auth

Block the auth probe from using ambient session cookies when a browser Origin does not match the request Host. Explicit bearer, body, and x-api-key tokens continue to work for cross-origin SDK calls.

Co-authored-by: Codex <[email protected]>

* Normalize origin checks for cookie auth

Compare normalized origins for browser cookie authentication so default ports and protocol mismatches are handled consistently. Add coverage for default-port and protocol-mismatch cases.

Co-authored-by: Codex <[email protected]>

---------

Co-authored-by: Codex <[email protected]>
2026-05-12 13:40:25 -07:00
Nariman Jelveh 8ad1a58f98 Reorder OIDC Apple and Microsoft buttons 2026-05-12 13:35:18 -07:00
Daniel Salazar 17e475236a fix: mistral ai tests from renames (#3091) 2026-05-12 12:28:32 -07:00
Daniel Salazar b948eaaaa6 chore: update mistralAi providers (#3090) 2026-05-12 12:12:24 -07:00
Nariman Jelveh 9b0c034247 fix: correct xAI TTS pricing to $15.00/1M characters (#3089)
xAI's Voice pricing table lists Text to Speech at $15.00 / 1M characters,
but we were charging $4.20 / 1M characters (420 microcents/char), undercharging
by ~72%. Update the cost constant, listEngines pricing, and test expectation
to 1500 microcents/char.
2026-05-12 11:57:11 -07:00
Miika Kuisma b0dc9c7f1b Merge pull request #3086 from HeyPuter/puter-js-update-260512
PuterJS menubar web component improvements
2026-05-12 12:01:08 +03:00
Miika Kuisma e79abd43e3 PuterJS menubar web component improvements 2026-05-12 12:00:15 +03:00
Miika Kuisma 36c9c478ec Merge pull request #3073 from HeyPuter/puter-js-tests
Puter JS Web Component update
2026-05-12 09:56:58 +03:00
Daniel Salazar 90d01b87ea fix: potential fix for some linux distros (#3085) 2026-05-11 23:28:59 -07:00
Daniel Salazar b0823449bc fix: don't allow dav access control credentials (#3084) 2026-05-11 19:11:02 -07:00
ProgrammerIn-wonderland 760d5d346d add missing GPT 5.4 models (#3083) 2026-05-11 21:34:23 -04:00
Daniel Salazar 4e11d1dd0f fix: updaet deepseek models (#3082) 2026-05-11 18:01:35 -07:00
ProgrammerIn-wonderland 5e8c66142d ai image video fs output (#3079)
* Add puter_output_path

* Fix error reporting in puterjs. Add precheck for puterOutputPath

* add tests

* add tests

* frontend puterjs tests
2026-05-11 20:55:45 -04:00
Daniel Salazar f80016e4e6 feat: rate limit all ai requests (#3081)
* chore: cleanup types for metering

* fix: error responses

* wip: rate limit refactor

* rate-limit for some drivers

* fix: ai driver limits

* tests: rate limiting
2026-05-11 16:18:21 -07:00
Daniel Salazar c8fdbdf8c9 fix: self host mysql (#3080) 2026-05-11 15:36:16 -07:00
ProgrammerIn-wonderland a04267a4e3 Add login with microsoft (#3077) 2026-05-11 15:40:44 -04:00
Daniel Salazar 80a081d62a fix: sql migrations (#3078) 2026-05-11 12:29:56 -07:00
Daniel Salazar bb131c6690 fix: oss icons for new apps (#3076) 2026-05-11 10:52:52 -07:00
Daniel Salazar 4a4a131226 fix: add validation for bad username password input (#3075) 2026-05-11 10:35:12 -07:00
Daniel Salazar 26cff23af4 chore: cleanup types for metering (#3074)
* chore: cleanup types for metering

* fix: error responses
2026-05-11 08:58:13 -07:00
Miika Kuisma 189dae25ef remove hover effect from menubar shortcut labels 2026-05-11 17:58:23 +03:00
Miika Kuisma b2f4a86224 Hide keyboard shortcuts from mobile phone menubar 2026-05-11 11:47:10 +03:00
Miika Kuisma 122811eb7a Puter JS menubar web component now accepts shortcut attribute, improven submenu ux 2026-05-11 11:41:30 +03:00
Miika Kuisma bc98dacf65 Merge branch 'main' into puter-js-tests 2026-05-11 09:55:35 +03:00
Daniel Salazar f00445254d oss: add more apps into oss (#3072) 2026-05-10 22:38:47 -07:00
Daniel Salazar 6686e3a0d7 tests: add tests for SpeechToTextDriver (#3071)
closes #3001
2026-05-10 22:27:35 -07:00
ProgrammerIn-wonderland 5bcb425926 Add tests for Peer, WebDAV, Workers, and WISP. (#3070) 2026-05-10 21:44:30 -04:00
Nariman Jelveh b4e9fa7688 Add dark mode colors to dev console CTA 2026-05-10 17:43:06 -07:00
Daniel Salazar 594cbe03ef tests: big test push with lots of missing areas (#3067)
- Strengthen Broadcast tests
- strengthen oidc tests
- add middleware tests
- add extensions test
- add puter hosting tests
- add Chat Completion Driver tests
2026-05-10 15:06:48 -07:00
Daniel SalazarandClaude Opus 4.7 ec04afe0ad tests: add unit tests for XAITTSProvider (#3060)
Adds offline XAITTSProvider.test.ts covering voice/format selection,
request shape, error paths, and cost reporting. Spies on global fetch
(the provider's egress point) against a real PuterServer + live
MeteringService.

Closes #2998

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-10 14:28:04 -07:00
Daniel SalazarandClaude Opus 4.7 539c928920 tests: add unit tests for ElevenLabsTTSProvider (#3066)
Adds offline ElevenLabsTTSProvider.test.ts covering voice/format
selection, request shape, error paths, and cost reporting. Spies on
global fetch (the provider's egress point) against a real PuterServer
+ live MeteringService. The companion integration test stays untouched.

Closes #2999

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-10 14:27:55 -07:00
Daniel SalazarandClaude Opus 4.7 a470857d4e tests: add unit tests for OpenAITTSProvider (#3057)
Adds offline OpenAITTSProvider.test.ts covering voice/format selection,
request shape, error paths, and cost reporting. Mocks the OpenAI SDK at
the module boundary against a real PuterServer + live MeteringService.
The companion integration test stays untouched.

Closes #3000

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-10 14:27:45 -07:00
Daniel SalazarandClaude Opus 4.7 dea40ca8be tests: add unit tests for TogetherVideoProvider (#3058)
Adds offline TogetherVideoProvider.test.ts covering parameter mapping
(togetherai: prefix stripping, seconds default vs no_extra_params,
width/height/fps/steps, reference_images / frame_images filtering),
polling for queued → in_progress → completed jobs (under fake timers),
failure / cancellation / missing-url error paths, and per-video
metering. Mocks together-ai at the module boundary against a real
PuterServer + live MeteringService.

Closes #2994

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-10 14:27:37 -07:00
Daniel SalazarandClaude Opus 4.7 3d593f7cd7 tests: add unit tests for OpenAIVideoProvider (#3059)
Adds offline OpenAIVideoProvider.test.ts covering parameter mapping
(size and seconds snapping to allowed values, input_reference
forwarding), polling for queued/in_progress jobs (under fake timers),
sora-2-pro size tiers (xl/xxl per-second pricing), per-second metering
on default tier, failure handling, and error paths. Mocks the OpenAI
SDK at the module boundary against a real PuterServer + live
MeteringService.

Closes #2993

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-10 14:27:30 -07:00
Daniel SalazarandClaude Opus 4.7 1d36811dd6 tests: add unit tests for GeminiTTSProvider (#3061)
Adds offline GeminiTTSProvider.test.ts covering voice/format selection,
request shape (transcript framing + speechConfig), error paths, and
cost reporting (token-priced input + output:audio batching, including
the PCM-to-WAV wrapping path). Mocks @google/genai at the module
boundary against a real PuterServer + live MeteringService.

Closes #2997

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-10 14:27:21 -07:00
Daniel SalazarandClaude Opus 4.7 4d53faf5b6 tests: add unit tests for GeminiVideoProvider (#3065)
Adds offline GeminiVideoProvider.test.ts covering parameter mapping
(size → aspectRatio/resolution, isHighRes forcing 8s, reference_images
clamping to 3 with image/lastFrame suppression, data-URL parsing,
negative_prompt), polling the long-running operation (under fake
timers), tier-aware metering (default / :1080p / :4k suffixes),
content-filter handling, and error paths. Mocks @google/genai at the
module boundary against a real PuterServer + live MeteringService.

Closes #2992

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-10 14:27:14 -07:00
Daniel SalazarandClaude Opus 4.7 c101fe74ab tests: add unit tests for TTSDriver (#3064)
Adds offline TTSDriver.test.ts covering provider selection/dispatch
(args.provider, legacy driverAlias via Context.driverName, registered
fallback), voice/format param validation, error mapping, and metering
propagation (provider failures must not bill). Mocks each provider's
SDK / fetch boundary against a real PuterServer wired with credentials
for every TTS provider.

Closes #2995

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-10 14:27:08 -07:00
Daniel SalazarandClaude Opus 4.7 b4fad80a8c tests: add unit tests for AWSPollyTTSProvider (#3063)
Adds offline AWSPollyTTSProvider.test.ts covering voice/format
selection (caller-supplied, language-derived, default-per-engine
fallback), SSML handling (TextType=ssml routing), request shape to the
AWS SDK SynthesizeSpeechCommand, engine validation, error paths, and
cost reporting. Mocks @aws-sdk/client-polly at the module boundary
against a real PuterServer + live MeteringService.

Closes #2996

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-10 14:27:02 -07:00
Daniel SalazarandClaude Opus 4.7 41e0049045 tests: add unit tests for VideoGenerationDriver (#3062)
Adds offline VideoGenerationDriver.test.ts covering provider
selection/dispatch (args.provider, model-id resolution across the
unified catalog, Context.driverName fallback), parameter validation
(seconds + dimension snapping to model.durationSeconds /
model.dimensions, string coercion), error mapping (provider
HttpErrors pass through, SDK errors don't bill), and metering
propagation (driver-level dispatch reaches the provider's
incrementUsage). Mocks each provider's SDK boundary against a real
PuterServer wired with credentials for every video provider.

Closes #2991

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-10 14:26:56 -07:00
Daniel Salazar 4aaa09bb04 tests: more tests for drivers and perm service (#3056) 2026-05-10 13:46:52 -07:00
Daniel Salazar 3f2c45be26 cleanup: authcontroller with testable methods and tests (#3054)
* cleanup: authcontroller with testable methods and tests

* fix: types

Tested this on zenpacket, seems to all be working
2026-05-10 13:24:17 -07:00
jelveh f7202955ef Move TOS blurb and update copy 2026-05-09 19:08:17 -07:00
jelveh 07432b0967 Update UIWindowSignup.js 2026-05-09 19:00:42 -07:00
jelveh 065eeda72a Add 'Sign up with email' option for OIDC 2026-05-09 18:55:18 -07:00
jelveh 65ce9a67ef Include user-agent in OIDC payload 2026-05-09 15:50:40 -07:00
Nariman Jelveh 4996d7f37f Include user_agent in auth request data (#3053) 2026-05-09 09:25:57 -07:00
Daniel Salazar 86adf4ebc0 fix message at end of selfhost script (#3052) 2026-05-08 23:46:54 -07:00
Daniel Salazar 9a4a0ac9f1 tests: add unit tests for XAIImageProvider (#3045)
Mocks the OpenAI SDK at the module boundary, boots a real PuterServer
for the wired MeteringService, and exercises construction (xAI base
URL, missing-key throw), model catalog (default + verbatim list),
test_mode bypass, prompt validation, the credit gate (402 before SDK),
model resolution (alias and unknown-id fallback), and the success
path covering URL/b64_json branches plus per-image metering with the
expected microcent override.

closes #2988
2026-05-08 23:34:39 -07:00
Daniel Salazar 2543179ae7 tests: add unit tests for OpenAiImageProvider with mocked client (#3050)
Mocks the OpenAI SDK at the module boundary, boots a real PuterServer
for the wired MeteringService, and covers the three model families
the provider treats differently. Exercises construction, model catalog
(dall-e-2 default + verbatim list), test_mode bypass, prompt
validation, the credit gate (402 before SDK), DALL-E request shape
(allowedRatio snap, hd-only-on-DALL-E-3, per-size metering, url and
b64_json output), gpt-image-* token-priced metering (input + output
batched line items at the model rates), and the gpt-image-2 size
normalizer (pixel-budget snap to multiples of 16, 3:1 ratio clamp,
3840 edge cap).

closes #2990
2026-05-08 23:33:52 -07:00
Daniel Salazar 07a049c8ac tests: add unit tests for TogetherImageProvider (#3049)
Mocks the together-ai SDK at the module boundary, boots a real
PuterServer for the wired MeteringService, and exercises construction
(missing-key throw), model catalog (default + verbatim list), test_mode
bypass, prompt validation, the credit gate (402 before SDK), all three
pricing branches (per-MP × ratio, per-image, per-tier with resolution
map rewrite), request shape (togetherai: prefix stripping, dimension
snap to multiples of 8 with min 64, optional knob clamping/forwarding,
input_image → image_base64 aliasing), and output handling for both
url/b64_json branches plus the wrapped error message.

closes #2987
2026-05-08 23:33:46 -07:00
Daniel Salazar 0990fc2667 tests: add unit tests for ReplicateImageGenerationProvider (#3048)
Mocks the replicate SDK and util/secureHttp at the module boundary
(real network egress points), boots a real PuterServer for the wired
MeteringService. Covers construction (missing-key throw), model
catalog (flux-schnell default + verbatim list), test_mode bypass,
prompt validation, the credit gate (402 before SDK), per-image
billing (replicateId routing, aspect_ratio gcd formatting,
single-output metering, string vs array output extraction, missing
URL → 400), megapixel billing (run + output_mp batched components),
the param-aliasing/transform/filter machinery (drops disallowed
keys, renames steps→num_inference_steps + response_format→output_format,
injects go_fast=true default for flux-2-dev, appends ' MP' suffix to
flux-2-pro resolution), and the costs_go_fast pricing branch.

closes #2986
2026-05-08 23:33:39 -07:00
Daniel Salazar 98bb203c84 tests: add unit tests for GeminiImageProvider (#3047)
Mocks @google/genai at the module boundary, boots a real PuterServer
for the wired MeteringService, and covers both the generateContent
(Flash) and generateImages (Imagen) code paths. Exercises
construction (missing-key throw), model catalog (default + verbatim
list), test_mode bypass, prompt validation, the credit gate (402
before SDK), Flash request shape (aspect ratio, allowedRatio fallback,
inlineData → base64 data URL extraction), Flash 3-line metering
(input + output:text + output:image with the IMAGE-modality token
count), Imagen routing (generateImages, per-image metering,
empty-response and rai-filtered failure modes).

closes #2989
2026-05-08 23:33:34 -07:00
Daniel Salazar 0e82a0aed6 tests: add unit tests for ImageGenerationDriver (#3046)
Boots a real PuterServer with API keys for every image provider
configured (so the driver indexes them all), drives
server.drivers.aiImage directly, and mocks each provider SDK at the
module boundary so the driver's routing is exercised without real
network egress. Covers 401 without actor, 400 on unknown model,
catalog dedup + sort, list() and getReportedCosts shape, provider
routing (id-based, lowercased lookups, args.provider override,
Context.driverName fallback), ratio normalization (width/height and
aspect_ratio "w:h" forms), and the ai.log.image audit event firing
both before the upstream call and even when it fails.

closes #2984
2026-05-08 23:33:28 -07:00
Daniel Salazar 3dc9d521b4 tests: add unit tests for CloudflareImageProvider (#3051)
Boots a real PuterServer for the wired MeteringService, stubs global
fetch (the provider talks to the Cloudflare REST API directly with no
SDK). Covers construction, model catalog, test_mode bypass, prompt
validation, the credit gate (402 before fetch), request shape (JSON
body, account-scoped /ai/run/<model> endpoint, apiBaseUrl override,
multipart routing for requiresMultipart models, steps clamping
[1,50]), output extraction (binary image/* → base64 data URL, JSON
envelope image with output_format → MIME, http(s) URL passthrough,
success:false 400, non-2xx 400, missing image 400), and per-scheme
cost components (tile-plus-step and flux2-klein-9b-mp split).

closes #2985
2026-05-08 23:33:21 -07:00
Daniel Salazar 4dbbc7c7c9 tests: add unit tests for GroqAIProvider with mocked client (#3042)
Mocks the groq-sdk at the module boundary, boots a real PuterServer
for the wired MeteringService, and exercises construction, model
catalog/resolution, request shape (max_completion_tokens rename, tools
passthrough, content blanking when paired with tool_calls), non-stream
+ streaming output (including Groq's x_groq.usage envelope deviation),
error mapping, and moderation.

closes #2977
2026-05-08 23:06:23 -07:00
Daniel Salazar 31f22bc339 tests: add unit tests for ClaudeProvider with mocked Anthropic SDK (#3038)
Mocks @anthropic-ai/sdk at the module boundary, boots a real
PuterServer for the wired MeteringService/stores/FSService, and
exercises construction (long timeout for Opus 4.7 thinking), model
catalog/resolution, request shape (tool_choice locking, system message
extraction, OpenAI-shape tool_calls → Claude tool_use blocks, tool
role → user tool_result, opus-4.7 temperature drop, adaptive thinking
config, enabled budget thinking + temperature=1 for older Sonnet),
non-stream output (input/output/cache token metering), streaming via
the message_start/content_block_*/message_delta/message_stop event
graph for both text_delta and input_json_delta tool_use, and the
moderation throw.

closes #2958
2026-05-08 23:06:05 -07:00
Daniel Salazar 87f0294f73 tests: add unit tests for OpenAiChatCompletionsProvider (#3036)
Mocks the OpenAI SDK at the module boundary, boots a real PuterServer
for the wired MeteringService/stores/FSService, and exercises
construction, model catalog (responses_api_only filtering), argument
validation (messages-array gate, web_search routing — error when no
sibling, delegation when one is set), request shape (max_completion_
tokens rename, gpt-5 vs non-gpt-5 reasoning_effort/verbosity gating,
stream_options gating), non-stream + streaming output (cached-token
splitting), the moderations.create flagging branch (above and at the
0.8 threshold), and error mapping.

closes #2959
2026-05-08 23:04:55 -07:00
Daniel Salazar df9c55daf9 tests: add unit tests for OpenAiChatResponsesProvider (#3037)
Mocks the OpenAI SDK at the module boundary, boots a real PuterServer
for the wired MeteringService/stores/FSService, and exercises
construction, the responses_api_only-filtered model catalog (with
no_restrictions toggle), argument validation (messages-array gate),
request shape (input messages, max_output_tokens rename, function-tool
unraveling, Responses-only knobs, gpt-5 vs non-gpt-5 reasoning controls),
model resolution, non-stream output (output_text → message.content,
function_call items → tool_calls, empty-response 400 gate), streaming
(response.output_text.delta text events, response.completed usage,
response.output_item.done function_call → tool_use), moderation, and
error mapping.

closes #2960
2026-05-08 23:01:45 -07:00
Daniel Salazar 9246aff4e4 tests: add tests for VoiceChangerDriver (speech2speech) (#3039)
Boots a real PuterServer with an ElevenLabs apiKey configured, drives
server.drivers.aiSpeech2Speech, and stubs global fetch (the real
network egress for ElevenLabs). Covers getReportedCosts mirroring
costs.ts, argument validation (test_mode bypass, missing actor 401,
missing audio 400), the credit gate (402 before fetch), the success
path (POST URL with voice id, default + override voice/model, optional
knobs forwarded via FormData and search params, audio stream return
shape), per-second metering math, and error mapping when ElevenLabs
returns a non-2xx body.

closes #2963
2026-05-08 23:01:36 -07:00
Daniel Salazar 21c37e6d35 tests: add unit tests for DeepSeekProvider with mocked client (#3040)
Mocks the OpenAI SDK at the module boundary, boots a real PuterServer
for the wired MeteringService, and exercises construction, model
catalog/resolution, request shape (max_tokens, tools, stream_options,
DeepSeek's content-blanking and tool-result system-message workarounds),
non-stream + streaming output paths, error mapping, and moderation.

closes #2976
2026-05-08 23:01:28 -07:00
Daniel Salazar cbdad4a65e tests: add unit tests for TogetherAIProvider with mocked client (#3041)
Mocks the together-ai SDK at the module boundary, boots a real
PuterServer for the wired MeteringService, and clears the kv-cached
model list between tests. Covers construction, model catalog (kv
caching, embedding-type filtering, alias flattening, synthetic
model-fallback-test-1), request shape (togetherai: prefix stripping,
max_tokens omission, tools/stream_options gating), model resolution,
non-stream + streaming output (with input/output cost-key remap),
error mapping, and moderation.

closes #2981
2026-05-08 23:01:20 -07:00
Daniel Salazar 91ecf54746 tests: add unit tests for OpenRouterProvider with mocked client (#3043)
Mocks both the OpenAI SDK and axios at the module boundary, boots a
real PuterServer for the wired MeteringService, and clears the
kv-cached model list between tests. Covers construction (default and
custom apiBaseUrl), model catalog (axios fetch + kv caching, openrouter:
prefix on ids, openrouter/auto filtering), the openrouter/auto disallow
gate, request shape (prefix stripping, usage:{include:true}, stream
options, retry-without-max_tokens on context-length errors, rethrow on
unrelated errors), both cost-calculator branches (cost-bearing vs
per-token fallback) for non-stream and streaming, and moderation.

closes #2980
2026-05-08 23:01:12 -07:00
Daniel Salazar 4bcfe6c7f4 tests: add unit tests for GeminiChatProvider with mocked client (#3044)
Mocks the OpenAI SDK at the module boundary (Gemini speaks the
OpenAI-compat dialect at the v1beta/openai endpoint), boots a real
PuterServer for the wired MeteringService, and exercises construction,
model catalog/resolution, request shape (max_completion_tokens rename,
cache_control stripping, stream_options gating), non-stream + streaming
output (cached-token splitting in the metered usage shape), error
mapping, and moderation.

closes #2961
2026-05-08 23:01:08 -07:00
Daniel SalazarandClaude Opus 4.7 3ada6f1569 tests: XAIProvider uses setupTestServer + real metering:[#2982] (#3035)
Drops the local makeMeteringStub in favour of a live PuterServer-wired
MeteringService. The OpenAI SDK is still mocked at the module boundary
(the real network egress point); the mock now also exposes a default
export so the test server can boot every chat provider (some import
the default and read .OpenAI off it). Aligns with AGENTS.md: "Prefer
test server over mocking deps."

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-08 19:21:52 -07:00
Daniel SalazarandClaude Opus 4.7 64402b01db tests: add tests for OCRDriver: [#2964] (#3028)
* tests: add tests for OCRDriver: [#2964]

Adds offline OCRDriver.test.ts covering both providers:

  • test_mode short-circuit; argument validation (missing actor, missing
    source, unknown provider, AWS/Mistral not configured)
  • aws-textract: raw-bytes vs S3Object source selection (regional
    client when fsEntry has a bucket), block normalisation (PAGE/WORD/
    TABLE filtered, LINE/LAYOUT_TITLE → text/textract:* blocks),
    402 on insufficient credits, per-page metering
  • mistral: image vs PDF chunk packaging (image_url with base64 data
    URL vs document_url with documentName), pass-through of pages /
    annotation / image-limit options, markdown → LINE-block
    normalisation with page indices, per-page metering, additional
    annotations metering when bbox/document annotation formats are set
  • default-provider selection (AWS preferred → Mistral fallback →
    500 when neither is configured)
  • getReportedCosts mirrors costs.ts

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* tests: OCRDriver uses setupTestServer + real fs/metering

Drops the manual config/clients/stores/services stub apparatus and
the loadFileInput mock in favour of the live wired driver from
server.drivers.aiOcr. The Textract and Mistral SDKs are still mocked
at the module boundary (the real network egress points); inputs go
through the real loadFileInput against real fs/store wiring (data
URLs for most cases; FSService.write produces a real fsEntry for the
PDF documentName test). Aligns with AGENTS.md: "Prefer test server
over mocking deps."

The S3Object-source/regional-client assertion was dropped because it
isn't deterministic against the in-memory S3 store and the driver's
per-region TextractClient cache leaks across tests. That branch is
better exercised by a real-cloud integration test.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-08 19:15:57 -07:00
Daniel SalazarandClaude Opus 4.7 6226db73d4 tests: add unit tests for MoonshotProvider with mocked client: [#2979] (#3030)
* tests: add unit tests for MoonshotProvider with mocked client: [#2979]

Adds offline MoonshotProvider.test.ts covering construction, model
resolution and aliasing, request shape (hardcoded max_tokens=1000,
stream_options gating, tool_use → tool_calls hoisting), vision-model
image-inlining hook, non-stream and streaming output, error logging
and rethrow, and metering. Existing integration test is left
untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* tests: MoonshotProvider uses setupTestServer + real metering

Drops the local makeMeteringStub in favour of a live PuterServer-wired
MeteringService. The OpenAI SDK is still mocked at the module boundary
(the real network egress point); inlineHttpImageUrls is mocked because
verifying the provider invokes it for vision-capable models is the
unit's concern. Aligns with AGENTS.md: "Prefer test server over
mocking deps."

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-08 19:15:45 -07:00
Daniel SalazarandClaude Opus 4.7 6ab2a2a6cc tests: add unit tests for MistralAiProvider with mocked client: [#2978] (#3032)
* tests: add unit tests for MistralAiProvider with mocked client: [#2978]

Adds offline MistralAiProvider.test.ts covering construction, model
resolution and aliasing, request shape (camelCase maxTokens, tool_calls
→ toolCalls and tool_call_id → toolCallId rewrite, complete-vs-stream
routing), non-stream output with the camelCase usage coercion,
streaming with the chunk.data unwrap and toolCalls deviation, error
mapping, and metering. Existing integration test is left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* tests: MistralAiProvider uses setupTestServer + real metering

Drops the local makeMeteringStub in favour of a live PuterServer-wired
MeteringService. The Mistral SDK is still mocked at the module
boundary (the real network egress point); everything else runs through
the real wired graph. Aligns with AGENTS.md: "Prefer test server over
mocking deps."

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-08 19:15:34 -07:00
Daniel SalazarandClaude Opus 4.7 ba03fd7965 tests: add unit tests for PuterAIController: [#2965] (#3031)
* tests: add unit tests for PuterAIController: [#2965]

Adds offline PuterAIController.test.ts with stubbed drivers covering:
route registration (paths and per-route auth options), app-actor
gating across all four chat-proxy routes, body validation (messages
must be an array; openaiCompletions prompt must be a string;
openaiResponses provider must be openai-responses), driver delegation
and request shaping (model + messages + provider defaults), response
shaping for OpenAI chat-completion / text-completion / Responses
envelopes and Anthropic message envelope (text + tool_use blocks),
SSE streaming with [DONE] terminator, model-listing endpoints with
hidden-id filtering and 501 when list() is unavailable, and the
HMAC-gated video proxy's input/expiry/secret guards.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* tests: PuterAIController uses setupTestServer + spy on aiChat.complete

Drops the manual driver/controller stub apparatus (makeStubDriver,
makeController, custom dep builders) in favour of the live wired
controller from server.controllers.puterAi. Each test that needs a
canned chat completion result spies on server.drivers.aiChat.complete,
which keeps the unit-of-test focused on the controller (validation,
response shape, SSE streaming) without dragging in chat-driver model
resolution and credit checks. Aligns with AGENTS.md: "Prefer test
server over mocking deps."

The "500 when URL signature secret missing" assertion was dropped —
the default config wires a secret, so the branch is unreachable in
the live test server. Replaced with the equivalent 403-on-invalid-
signature assertion which actually exercises the HMAC gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-08 19:15:23 -07:00
Daniel SalazarandClaude Opus 4.7 1c188d6325 tests: add unit tests for ZAIProvider with mocked client: [#2983] (#3029)
* tests: add unit tests for ZAIProvider with mocked client: [#2983]

Adds offline ZAIProvider.test.ts covering construction, model
resolution and aliasing, GLM-specific request shape (custom params,
cache_control stripping, user_id derivation, stream_options gating),
non-stream output with reasoning_content normalisation, streaming
deltas (text and tool_use), error mapping, and metering. Existing
integration test is left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* tests: ZAIProvider uses setupTestServer + real metering

Drops the local makeMeteringStub in favour of a live PuterServer-wired
MeteringService. The OpenAI SDK is still mocked at the module boundary
(the real network egress point); everything else — config, Context,
metering recording — runs through the real wired graph. Aligns with
AGENTS.md: "Prefer test server over mocking deps."

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-08 19:14:15 -07:00
ProgrammerIn-wonderland 8b1226aa1b OIDC referrer (#3034)
* oidc referrer

* optionally infer referrer from window.openerOrigin

* infer referrer from openerOrigin if there

* add log in signupCb

* remove excess logs

* remove FSEntry.js
2026-05-08 22:05:09 -04:00
Daniel Salazar 2c1d320bba ci: push to dockerhub too (#3033) 2026-05-08 17:36:21 -07:00
Daniel Salazar b6414442dd fix: contributing (#3027) 2026-05-08 16:58:05 -07:00
Daniel Salazar f5470f123f tests: add tests for xaiProvider: [#2982] (#3026) 2026-05-08 16:09:25 -07:00
Nariman Jelveh ea9a44c9ad Implement inline 2FA UI, remove old components (#3025)
* Implement inline 2FA UI, remove old components

* Re-enable login button on window close
2026-05-08 15:59:40 -07:00
jelveh 881ee90e76 Remove stray fullscreen hint from translations 2026-05-08 15:16:47 -07:00
jelveh fbb570fb36 Reapply "Remove toolbar clock feature (#3024)"
This reverts commit e781b60bcb.
2026-05-08 15:15:03 -07:00
jelveh e781b60bcb Revert "Remove toolbar clock feature (#3024)"
This reverts commit 78a071ae54.
2026-05-08 15:11:55 -07:00
Nariman Jelveh 78a071ae54 Remove toolbar clock feature (#3024) 2026-05-08 15:08:15 -07:00
Daniel Salazar 9bcd77c85b chore: add legacy codes back to all errors (#3022) 2026-05-08 13:51:55 -07:00
Daniel Salazar 960bbb6965 doc: code architecture doc + contributing guide for backend (#2953)
* architecture doc

* agents and contributor md guides

* fix: userStore
2026-05-08 13:51:41 -07:00
Nariman Jelveh 46a56e474b README edit (#3012)
* Add 'For Users' and 'For Developers' sections

* Update README.md

* Update README.md

* Update README.md
2026-05-08 13:30:33 -07:00
Reynaldi Chernando 666b60bd29 Use GA version of gemini 3.1 flash lite (#3013)
* Use GA version of gemini 3.1 flash lite

* add model name alias
2026-05-08 13:51:56 -04:00
Daniel Salazar 63f128457e remove driver usage endpoint (#3017) 2026-05-08 10:18:26 -07:00
Daniel Salazar 5e8e9a852d test: add unit tests for ai-chat utils (#3016)
Covers `Messages` (normalize_single_message / normalize_messages /
extract_and_remove_system_messages / extract_text), `FunctionCalling`
(normalize_json_schema / normalize_tools_object /
make_openai_tools / make_claude_tools), and `Streaming`
(AIChatStream / AIChatMessageStream / AIChatTextStream /
AIChatToolUseStream). All three are pure data transforms — Streaming
is exercised against a real `AIChatStream` wired to a buffering
Writable, so no method-level spies are needed.

Closes #2956
2026-05-08 10:17:46 -07:00
Daniel Salazar 3166ad933c test: add unit tests for OpenAIUtil shape normalization (#3015)
Covers the Puter↔OpenAI shape conversion (`process_input_messages`,
`process_input_messages_responses_api`), usage extraction
(`extractMeteredUsage`, `create_usage_calculator`), the streaming
handlers (`create_chat_stream_handler` and the responses-API variant),
and the non-stream `handle_completion_output[_responses_api]` paths
including moderation, missing-content, and per-provider deviations.
Streaming is exercised via a real `AIChatStream` wired to a buffering
Writable so no method-level spies are needed.

Closes #2955
2026-05-08 10:17:42 -07:00
Reynaldi Chernando 7e5f64e04b Fix minor typo for tls cert in self hosting docs (#3014) 2026-05-08 09:23:06 -07:00
jelveh 908c02fd99 fix ASCII art for PUTER.JS 2026-05-08 08:04:21 -07:00
jelveh 38fcc6d6bd Detect fullpage apps early and skip desktop load 2026-05-07 22:17:27 -07:00
Daniel Salazar 16652347c6 test: add tests for HomepageController (#3010)
Covers the early-return guard when the homepage service is unwired,
shell rendering at /, /settings, /dashboard, /action, /@:username,
canonical_url + meta inheritance from gui_params, /app/:name (404 +
shell, embedded app metadata, JSON / object metadata, malformed JSON),
and /show/* launching the explorer with the trimmed path.

Closes #2973
2026-05-07 21:41:16 -07:00
Daniel Salazar 5f28b988ec test: add tests for SystemController (#3009)
Covers /healthcheck (live status, 503 on failure, fallback when service
unavailable), /version (semver parsing, env defaults), /contactUs
(validation, persistence, email best-effort, default support address),
/whoarewe, /lsmod (interface grouping + driverName fallback), and the
prepareShutdown drain-flag flip.

Closes #2971
2026-05-07 21:40:27 -07:00
Daniel Salazar 160631b158 test: add tests for OIDCController (#3008)
Covers the provider listing route, the auth-start redirect (404 / 400 /
500 / 302), all three callback flows (login / signup / revalidate)
including state validation, suspended accounts, unconfirmed-email
linking refusal, redirect_uri origin clamping, and the
revalidate-done landing page. Routes are collected via PuterRouter and
invoked with stubbed services that return prefab data.

Closes #2966
2026-05-07 21:38:47 -07:00
Daniel Salazar 3ab9923f0c fix: ai provider integration tests (#3011)
* fix: ai provider integration tests

* change name for ai test
2026-05-07 21:38:40 -07:00
Daniel Salazar cc799e069f test: add unit tests for fileInput driver utility (#3007)
Covers `loadFileInput` (validation, data-URL decoding, FS-path resolution,
maxBytes enforcement, ACL gating) and `inferFilenameFromUrlOrPath`. Stubs
stores/fsService with prefab data so tests don't require a real server.

Closes #2957
2026-05-07 21:38:27 -07:00
Daniel Salazar 9a7d3bac5f test: add tests for DesktopController (#3006)
Covers /set-desktop-bg (partial updates, null clearing, type validation),
/update-taskbar-items (JSON serialization), /set_layout (layout
allowlist, ownership gate, path/uuid resolution), and /set_sort_by
(sort_by/sort_order allowlists, default ordering). Routes are
collected via PuterRouter and driven with stub stores returning
prefab fsentry rows.

Closes #2970
2026-05-07 21:37:34 -07:00
Daniel Salazar 0a9a4f629d test: add tests for StaticPagesController (#3005)
Covers /robots.txt (bot disallow + sitemap), /sitemap.xml (docs +
approved-listing apps), /unsubscribe (missing/invalid uuid, idempotent
re-runs), and /confirm-email-by-token (token mismatch, already-confirmed
short-circuit, duplicate-email rejection, success path with verified-
group promotion + best-effort side channels). Stub stores hold prefab
user rows; the DB stub returns canned EXISTS / SELECT results.

Closes #2975
2026-05-07 21:36:50 -07:00
Daniel Salazar 32c2c0c1a5 test: add tests for AppController (#3004)
Covers /apps, /apps/nameAvailable, /rao, /apps/:name (single + batch),
/query/app (max-entries cap, marketplace shape, hidden-app gating), and
/app-icon (default fallback, data-URL decoding, MIME allowlist, CDN
redirect). Routes are collected via PuterRouter; AppDriver is injected
through the shared `driversContainers` registry, and stub stores return
prefab app rows.

Closes #2969
2026-05-07 21:35:49 -07:00
Daniel Salazar 508d652194 test: add tests for StaticAssetsController (#3003)
Covers each config-gated branch (client_libs_root, puterjs_root,
gui_assets_root, builtin_apps) using real temp directories so the
existsSync checks see real files instead of mocked module behavior.
Asserts both registration outcomes (which routes appear, on which
subdomains) and the file resolution rules for the puter.js / puter.dev.js
fallback.

Closes #2974
2026-05-07 21:35:17 -07:00
jelveh 471ba94ba2 Update index.js 2026-05-07 20:04:43 -07:00
jelveh 66e24a07d4 Update console banner and messaging 2026-05-07 19:49:01 -07:00
jelveh 5ffac54c86 experimental cute CTA 2026-05-07 19:39:06 -07:00
ProgrammerIn-wonderland c5b12b61d9 Unconditionally send Access-Control-Allow-Private-Network, fix LNA fix in GUI (#2954) 2026-05-07 16:38:04 -07:00
Daniel Salazar 8d5e9ef9cf fix: types for open ai driver (#2950)
* fix: types for open ai driver

* fix: add extra time to tests

* fix: test

* fix: together ai tests

* fix: tests
2026-05-07 14:48:47 -07:00
Daniel Salazar 25fe9a3cdf fix: add coverage reports for changes (#2949)
* tests: ai integraton tests

* fix: add coverage reports for this too
2026-05-07 13:55:09 -07:00
Daniel Salazar 05425d20a7 chore: cleanup types for tests (#2948) 2026-05-07 13:40:58 -07:00
Nariman Jelveh ff10e47d67 Show fullscreen overlay when signup is blocked (#2947) 2026-05-07 11:37:23 -07:00
Daniel Salazar 6c19dfbafd fix: linux build (#2946) 2026-05-07 11:23:10 -07:00
Nariman Jelveh 360cbf48af feat: support code in puter.signup.validate error responses (#2945)
Extensions listening on the `puter.signup.validate` event can now set a
`code` field (in addition to `message`) when blocking signups. The code
is forwarded through HttpError and appears in the JSON error response,
giving clients a stable, machine-readable signal to act on.

Covers both the AuthController (password signup) and OIDCService paths.
Adds tests for the new field.
2026-05-07 10:11:08 -07:00
Daniel Salazar bbe93defe9 tests: add some more driver tests (#2944) 2026-05-07 10:04:39 -07:00
Reynaldi Chernando 5e01c13b31 Fix duplicate textract result (#2943) 2026-05-07 09:15:28 -07:00
Daniel Salazar 4b37f35803 test: tests for fs endpoints (#2942) 2026-05-06 22:55:53 -07:00
Daniel Salazar 775c9c49e2 test: add harness and some examples (#2941)
* test: add harness and some examples

* feat: tests ran on pr
2026-05-06 22:43:22 -07:00
Nariman Jelveh e35260924e Scope .myapps-search to input and tweak UI styles 2026-05-06 17:59:54 -07:00
Nariman Jelveh 90aad8918c Support light/dark sidebar logo 2026-05-06 17:54:37 -07:00
Nariman Jelveh 46974beba1 Replace gradients with flat colors in system buttons 2026-05-06 17:47:45 -07:00
Shruc 5e9dc33be1 expose costs in ai chat response (#2921)
* expose costs

* microcents
2026-05-06 15:59:16 -07:00
Daniel Salazar 1e271f188e fix: bring back old app names functionality and fix build (#2937) 2026-05-06 15:48:25 -07:00
jelveh f333597754 Update IPC.js 2026-05-06 15:40:38 -07:00
jelveh 3b33d16356 Merge branch 'main' of https://github.com/HeyPuter/puter 2026-05-06 15:33:21 -07:00
jelveh 2289d61d3c Update UIPrompt.js 2026-05-06 15:33:13 -07:00
Daniel Salazar 8e02f38bad fix: templates (#2936) 2026-05-06 15:31:41 -07:00
angelinazzz1809 5b65a6bc78 Update UIPrompt.js (#2930)
In the app environment, the prompt input was initialized using `options.value`, so `defaultValue` was not reflected.

This change ensures that `defaultValue` is used when provided, while keeping backward compatibility with `options.value`.
2026-05-06 15:27:28 -07:00
ProgrammerIn-wonderland 872d2c01d4 Add support for log in with Apple (#2935)
* Add experimental login with Apple support

* Fix revalidate URL

* Fix OIDC provider revalidate in whoami as well
2026-05-06 16:47:24 -04:00
jelveh b7f199ddbc Normalize string options to object 2026-05-06 11:59:28 -07:00
jelveh 906c00af97 Support options.defaultColor in color picker 2026-05-06 11:48:47 -07:00
Shruc f00d52f879 per model allowed params on replicate image, normalize ratio (#2898)
* replicate allowed params, normalize ratio

* update docs
2026-05-06 14:16:30 -04:00
Devendra Reddy Pennabadianddevareddy05 7a1a885e89 fix: inline http(s) image URLs for Moonshot vision models (#2929) (#2931)
Co-authored-by: devareddy05 <[email protected]>
2026-05-06 13:53:52 -04:00
Daniel Salazar ff8c275317 chore: package-lock.json (#2934) 2026-05-06 10:52:22 -07:00
Devendra Reddy Pennabadianddevareddy05 b3bf979cac fix: support defaultValue in showColorPicker app env (#2895) (#2932)
Co-authored-by: devareddy05 <[email protected]>
2026-05-06 10:46:32 -07:00
Reynaldi Chernando eb9e316ed5 Update Z.AI vision model id to z-ai/z-ai for consistency (#2928) 2026-05-06 02:41:04 -04:00
Daniel Salazar 224ea5f7f8 fix: extensions (#2926) 2026-05-05 20:55:53 -07:00
Daniel Salazar c1dacab4c2 extension events after boot fixes (#2925) 2026-05-05 17:42:55 -07:00
Daniel Salazar 8d0b8d4905 fix: app errors (#2924) 2026-05-05 17:20:31 -07:00
jelveh e7190f442e Update README.md 2026-05-05 16:49:01 -07:00
Nariman Jelveh 95e1b7d171 New README.md (#2915)
* Update README branding, screenshot, and links

* Update README: rephrase intro and add goals

* Update README.md

* Update README.md

* Rename self-hosting docs and add Windows installer

* Update self-hosting headings and README link

* Update README.md

* Update README.md

* Update README.md
2026-05-05 16:35:07 -07:00
Daniel Salazar f9ac017faf fix: fsEntry and tscofnig (#2923) 2026-05-05 16:18:09 -07:00
Daniel Salazar 4252b7f76f fix: email (#2922) 2026-05-05 15:21:15 -07:00
Daniel Salazar 58a7459e45 fix: broken apps in self hosted (#2920) 2026-05-05 12:45:17 -07:00
ProgrammerIn-wonderland 5dd601a90b add back web input into OCR (#2919) 2026-05-05 14:44:54 -04:00
Daniel Salazar 50fe867e0d fix: fs issues [PUT-846] (#2918)
* fix: fs issues

* windows install script
2026-05-05 10:49:04 -07:00
Daniel Salazar 98bdaf0c42 add email into metering alert (#2917) 2026-05-05 10:18:45 -07:00
Nariman JelvehandClaude Opus 4.6 1f1149e32e Block unconfirmed users from API endpoints server-side (#2916)
Add a default-on email confirmation gate that rejects users with
`requires_email_confirmation && !email_confirmed` on all authenticated
routes, returning 403 with `email_confirmation_required`.

Previously this was only enforced client-side via a GUI modal, meaning
direct API calls could bypass the check entirely.

Essential routes are exempted via `allowUnconfirmed: true`:
  `/whoami`, `/logout`, `/send-confirm-email`, `/confirm-email`,
  `/save_account`, `/get-anticsrf-token`, `/get-gui-token`,
  `/session/sync-cookie`, `/auth/revoke-session`,
  `/user-protected/delete-own-user`

No impact on temp users (`requires_email_confirmation` is false),
self-hosted deployments without email (flag is never set), or
unauthenticated routes (login, signup, password recovery, OIDC).

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-05-05 10:08:50 -07:00
jelveh 51b81c82bf Update keyboard.js 2026-05-05 06:24:49 -07:00
Daniel Salazar 15201b7a3c fix: nginx missing in self host script (#2911) 2026-05-05 00:42:54 -07:00
Daniel Salazar ad3f0f6c2e fix: handle fs duplicates on write (#2910) 2026-05-05 00:33:55 -07:00
Daniel Salazar 838d6a7178 docker-compose from index (#2909) 2026-05-05 00:22:25 -07:00
Daniel Salazar a5cb484e52 cleanup config (#2908) 2026-05-04 23:12:34 -07:00
jelveh 3a252fb948 Rename moonshot provider key to moonshotai 2026-05-04 21:43:31 -07:00
jelveh 69365cbae4 Standardize Moonshot provider ID to moonshotai 2026-05-04 21:40:38 -07:00
jelveh 2f1b9aad06 Update UIDesktop.js 2026-05-04 21:09:48 -07:00
jelveh 9e8371bde6 Update UIDesktop.js 2026-05-04 21:03:53 -07:00
Daniel Salazar 704fcecbdf fix: selfhosted thumbnails extensions (#2905) 2026-05-04 19:56:10 -07:00
jelveh 785d8b0cc5 Remove recovery warning block from 2FA setup 2026-05-04 19:45:52 -07:00
jelveh eaa68c243a Remove fullscreen button and handlers 2026-05-04 18:56:51 -07:00
Daniel Salazar a24246bfbc selfhosting: p3 cleanup docs + small fixes again (#2904)
* fix: Perms and configs

* rm broken editor for now

* selfhosting cleanup
2026-05-04 18:47:37 -07:00
Daniel SalazarandCopilot 50679e6a01 selfhosting: p2 fixed default configs and guide + some other errors (#2903)
* fix: ollama support

* fix: more self host cleanup

* fix: bad auth

* fix: more session cookie stuff

* allow emailclient

Co-authored-by: Copilot <[email protected]>

* fix: broken migration

* fix: full-stack comment

* fix: hardcoded perms

* new docker build steps

---------

Co-authored-by: Copilot <[email protected]>
2026-05-04 18:14:23 -07:00
jelveh d8cc884f01 Revamp 2FA setup UI with new wizard 2026-05-04 17:49:38 -07:00
jelveh 6a534b8479 Add pending state for 2FA toggle 2026-05-04 17:13:23 -07:00
jelveh 4048dfbaf1 Remove 'Show Desktop' toolbar button 2026-05-04 16:26:59 -07:00
Daniel Salazar 2de8807942 selfhost: fullstack self host p1 (#2902)
* wip: self hosted full setup

* wip: self hoast modes

* more wip self host stuff

* wip: full release

* fix: custom apps

* fix: readme

* full-stack md

* docs update

* fix

* fix: commands

* remove comments

* fix: docs and migs

* mysql mig

* fix: docker changes

* fix: mysql checks

* fix: mysql mig

* fix: docker more

* fix: redis conn issues
2026-05-04 15:58:03 -07:00
Daniel Salazar 8642a5b58c fix: subdomain cache invalidaton (#2901) 2026-05-04 15:17:20 -07:00
Nariman Jelveh 36de2b9aa4 Remove settings UI & service; redirect to dashboard (#2900) 2026-05-04 14:11:56 -07:00
jelveh c6bacca7f6 Open dashboard in new tab from UI clicks 2026-05-04 12:26:39 -07:00
Reynaldi Chernando da0255a805 Update UI docs and types (#2854) 2026-05-04 14:43:24 +07:00
jelveh dee193502f Add txt2speech.listEngines and listVoices docs 2026-05-03 22:12:49 -07:00
jelveh b9458497fb Update package-lock.json 2026-05-03 19:17:44 -07:00
jelveh 52d2c8574f Update build.js 2026-05-03 18:53:37 -07:00
jelveh 3da16e98e3 Produce llms.txt and llms-full.txt outputs for docs 2026-05-03 18:48:29 -07:00
Nariman Jelveh 85336f3c0e Add Moonshot AI (Kimi) chat completion provider (#2893) 2026-05-03 15:15:41 -07:00
jelveh 9720516185 Bump puter.js version to 2.3.2 2026-05-03 11:38:40 -07:00
jelveh 2a0efcff87 Bump puter.js version and update homepage 2026-05-03 11:34:11 -07:00
jelveh f9143a05c2 Add Gemini/xAI TTS and xAI STT examples 2026-05-03 11:24:38 -07:00
Nariman Jelveh bd30cda4a9 Add xAI (Grok) TTS and STT providers and integrate client/docs (#2890)
- Add XAITTSProvider calling POST /v1/tts with 5 voices (eve, ara, rex,
    sal, leo), speech tags, and configurable output format
  - Add XAISpeechToTextDriver calling POST /v1/stt with support for
    diarization, multichannel, text formatting, and xAI's native URL
    param for remote audio files
  - Register both providers in TTSDriver and driver index using existing
    xAI API key from config
  - Update puter.js client to route provider:'xai' for both txt2speech
    and speech2txt, including listVoices/listEngines
  - Add TypeScript types for new STT options (provider, format, diarize,
    multichannel, etc.)
  - Update txt2speech.md and speech2txt.md with xAI options and examples
  - Add playground examples for both TTS and STT

  Cost metering:
    TTS: 420 microcents/char ($4.20/1M chars)
    STT: 2778 microcents/sec ($0.10/hr)
2026-05-03 11:00:31 -07:00
Nariman Jelveh 33d5c0737e Add Gemini TTS provider and integrate client/docs (#2889)
Introduce a new Gemini TTS provider and wire it through the driver, client, docs, and examples. Adds src/backend/drivers/ai-tts/providers/gemini/GeminiTTSProvider.ts (Google GenAI client usage, PCM->WAV wrapping, metering, model/voice validation) and a costs table in providers/gemini/costs.ts. Registers the provider in TTSDriver, exposes the alias gemini-tts, and prefers it in provider selection. Updates puter-js client to recognize "gemini" provider/engine and route driver calls to gemini-tts. Documentation updated with Gemini options, a usage example, and a playground example HTML file.
2026-05-03 09:33:50 -07:00
Daniel Salazar cb27fd1f98 Revert "Revert "remove exports (#2885)" (#2887)" (#2888)
This reverts commit 14cde26291.
2026-05-02 10:20:01 -07:00
Nariman Jelveh d4ef956660 Remove RecoveryCodeEntryView and inline UI 2026-05-02 10:14:27 -07:00
Nariman Jelveh 789c62ef71 Remove RecoveryCodesView and inline in 2FA setup 2026-05-02 09:55:12 -07:00
Nariman Jelveh b2a70a6a38 Remove ConfirmationsView and inline checkboxes 2026-05-02 09:52:09 -07:00
Nariman Jelveh dd9607b54e Remove StepHeading component and inline headings 2026-05-02 09:46:44 -07:00
Daniel Salazar 14cde26291 Revert "remove exports (#2885)" (#2887)
This reverts commit 4e94cb07be.
2026-05-02 09:41:42 -07:00
jelveh 388577a799 Update style.css 2026-05-02 08:30:27 -07:00
jelveh d0310ac211 Add Upgrade/Manage Plan button to Usage tab 2026-05-02 08:28:46 -07:00
jelveh 99babf3879 Remove System Information window 2026-05-02 08:03:05 -07:00
jelveh ffae2a1d0a Remove PuterDialog and ExportRegistrantService 2026-05-02 07:58:12 -07:00
jelveh ce69bc14a8 Remove groups manager UI extension 2026-05-02 07:53:38 -07:00
Daniel Salazar 4e94cb07be remove exports (#2885) 2026-05-01 23:52:52 -07:00
jelveh 4395c8bea5 Handle OIDC popup flow: skip session list 2026-05-01 23:49:35 -07:00
Daniel SalazarandCopilot 5b35a5db69 errors for fs (#2884)
* remove bloated tests and

* errors for fs

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Copilot <[email protected]>
2026-05-01 23:24:05 -07:00
jelveh c1ff8a3a9a Enable email confirmation code and style modal 2026-05-01 23:23:31 -07:00
jelveh c352dfe284 Send confirmation code and hide close button 2026-05-01 23:06:18 -07:00
jelveh 9f2ec29586 Prefer app upgrade prompt on storage errors 2026-05-01 21:07:49 -07:00
jelveh 63be2308d3 Show upgrade dialog on storage limit errors 2026-05-01 20:24:09 -07:00
jelveh e509f8ab46 Remove obsolete theme.css and its reference 2026-05-01 19:54:38 -07:00
jelveh b481753526 Remove obsolete GUI docs and .idx files 2026-05-01 19:46:20 -07:00
Daniel Salazar 1796fdb37b fix: misc fs issues (#2883)
* fix: deps

* fix: FS issues
2026-05-01 18:54:10 -07:00
jelveh 25f02378cb Update StaticPagesController.ts 2026-05-01 17:29:07 -07:00
jelveh e95e2f5187 Use full HTML template for static pages 2026-05-01 17:18:06 -07:00
jelveh 3f8365ad7e Add logout handler and disable modal dragging 2026-05-01 17:00:22 -07:00
ProgrammerIn-wonderland 8bd0d7c8c9 invalidate subdomains on delete (#2882) 2026-05-01 16:49:22 -07:00
Daniel SalazarandCopilot 2c261fc6c0 fix: extensions (#2881)
Co-authored-by: Copilot <[email protected]>
2026-05-01 16:36:15 -07:00
jelveh 0ffb7d3d1e Remove Amplify config; add nwsapi dep 2026-05-01 15:39:50 -07:00
jelveh 1305bf16e8 Add Amplify build config and update docs deps 2026-05-01 15:27:46 -07:00
jelveh d9cbdf3394 Update package.json 2026-05-01 15:00:40 -07:00
Daniel SalazarandCopilot a3b11de1a2 fix: better error codes + unlimited local credit (#2880)
* fix: better error codes

Co-authored-by: Copilot <[email protected]>

* fix: add in local unlimited metering again

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Copilot <[email protected]>
2026-05-01 14:43:13 -07:00
Nariman Jelveh cf3f943177 Delete license_header.txt (#2879) 2026-05-01 14:06:55 -07:00
Nariman Jelveh 267f464232 Add AGPL license headers to source files (#2877) 2026-05-01 13:50:42 -07:00
ProgrammerIn-wonderland b70aabdeae zai --> z-ai (#2878) 2026-05-01 13:39:25 -07:00
ProgrammerIn-wonderland 8936f79716 Ns/return better together video error (#2876)
* Return better together video error
2026-05-01 13:13:08 -07:00
ProgrammerIn-wonderland 968452a45e Add grok 4.3 (#2875) 2026-05-01 13:00:02 -07:00
jelveh 05c4b40497 Merge branch 'main' of https://github.com/HeyPuter/puter 2026-05-01 12:59:08 -07:00
Nariman Jelveh 60ea48dbaa Remove gross legacy tools and docs (#2873) 2026-05-01 12:58:36 -07:00
Nariman Jelveh 284ca86b7c Remove gross retarded legacy tools and docs (#2873)
* Remove gross retarded legacy tools and docs

* Remove tools scripts

* nuke the bs

* Remove contributor/self-hosting docs, update license

* Remove src/useapi package and update configs

* remove source map files

* Remove wiki submodule

* ok not everything is retarded, for example this one (v86) is cool but has to go for now

* Remove puter-wisp module

* Remove install.md

* Remove twisp submodule

* Remove epoxy-tls submodule

Remove the epoxy-tls submodule and its configuration: delete the gitlink at submodules/epoxy-tls and remove the corresponding entry from .gitmodules.
2026-05-01 12:32:41 -07:00
ProgrammerIn-wonderland 7cd762afa2 Add z.ai chat completions (#2874) 2026-05-01 12:23:23 -07:00
Daniel Salazar 1b39bd2a4a fix: app autocreation (#2872) 2026-05-01 09:03:42 -07:00
Daniel Salazar e8fbb88102 puter models list fix (#2869) 2026-05-01 01:41:56 -07:00
Daniel SalazarandCopilot f6e1f90225 fix: dbConfigs and ai routes (#2868)
* fix: db configs

Co-authored-by: Copilot <[email protected]>

* fix: ai routes

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Copilot <[email protected]>
2026-04-30 23:56:18 -07:00
Daniel SalazarandCopilot 0e53edafa3 fix: send right error codes (#2867)
Co-authored-by: Copilot <[email protected]>
2026-04-30 21:42:48 -07:00
Daniel Salazar 4a10124574 fix: oidc and chat validation events (#2866) 2026-04-30 21:05:07 -07:00
Nariman Jelveh d2555835c7 Remove email confirmation requirement in feedback 2026-04-30 21:04:39 -07:00
Nariman Jelveh 73d26e02b3 Only reset URL/title for owning windows 2026-04-30 20:54:17 -07:00
Daniel Salazar 48a85998ad fix: sync ips for validation (#2865)
* oidc email validate

* fix: sync ips for validation
2026-04-30 17:53:25 -07:00
Daniel Salazar 1a6b02999b fix: more clean ip (#2864) 2026-04-30 16:45:54 -07:00
Daniel Salazar 976f1de806 fix: event (#2863) 2026-04-30 16:37:27 -07:00
Daniel Salazar 1546e0e02f fix: auth checks (#2862)
* rename validate email

* fix: authService
2026-04-30 16:32:22 -07:00
ProgrammerIn-wonderland ce18b914b4 backport devwatcher (#2861) 2026-04-30 16:23:34 -07:00
Nariman Jelveh a54d6ceee5 Merge branch 'main' of https://github.com/HeyPuter/puter 2026-04-30 16:03:51 -07:00
Nariman Jelveh a32ac4e672 Add dynamic usage-bar colors and refine layout 2026-04-30 16:03:39 -07:00
Daniel SalazarandCopilot 93ed2068de fix: fail unauthed sessions nicely (#2860)
Co-authored-by: Copilot <[email protected]>
2026-04-30 16:01:23 -07:00
Daniel SalazarandCopilot 6b8152d3bc fix: cleanup some errors (#2859)
Co-authored-by: Copilot <[email protected]>
2026-04-30 15:10:19 -07:00
Daniel SalazarandCopilot eaa3d8c83c fix: no default gemini image quality (#2858)
Co-authored-by: Copilot <[email protected]>
2026-04-30 15:03:53 -07:00
Nariman Jelveh d772b557a0 Replace 2FA buttons with toggle switch 2026-04-30 15:01:06 -07:00
Nariman Jelveh 867d967b37 Use 'Current' badge and style active plan pill 2026-04-30 14:13:10 -07:00
Daniel SalazarandCopilot 56eef339a8 fix: ai driver errors (#2857)
Co-authored-by: Copilot <[email protected]>
2026-04-30 13:41:41 -07:00
Nariman Jelveh 15b93abe04 Set body overflow to hidden in dashboard CSS 2026-04-30 13:02:55 -07:00
Daniel Salazar 208abda841 fix: bad ai endpoint (#2856) 2026-04-30 12:38:39 -07:00
d4d78ac7db rework: change backend and backend extensions to use simpler code structure and patterns (#2815)
* fix:  dynamodb health checks and client recreation (#2789)

* wip: no nanoServices groundwork

* feat: data clients in new shape

* wip: auth and perms in new system

* more wip

* middlewaters mainly done

* wip: fsv2 in new layout

* old fs v2 migration

* driver system

* driver and old fs fixes

* ai drivers wip

* stream support

* metering in ai chat driver

* wip: new auth

* rate limit and auth routes

* captcha and anti csrf

* fix: types

* auth store

* app logic

* wip most other dricvers

* fs

* mostly kill all legacy stuff

* fs finish

* fix: redis usage

* ai controller

* driver cleanup

* socket io in v2

* broadcast and crudq stuff

* subdomains

* notifcations and shares

* fix bad syntaxes

* auth wip

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* extensions

* extension setup

* more routes

* sql migrations and default services

* home router

* tier 7

* everything else

* everything else

* remaining missing bits

* server health

* logs

* cleanup

* deps

* cleanup 2

* more cleanup 2

* boot

* fix launch

* config fix

* move file

* fix: tsconfig things

* fix: extension loading

* launching

* fix: drivers

* fix: others

* fix: icons

* fix: file uploads

* fs fixes

* fix: fs api

* fix: dev-center

* config

* add back telemetry

* lint stuff

* husky hooks

* fix: fs oss

* fix: config migration

* config migration

* migrate scripts + replicate

* runner

* fix: merge defafult config

* fix: default region

* fix: api domain

* fix paths in readfile

* fix fs entry default s3

* NS: Remove Referral && Entri Service

* dep cleanups

* fix: static assets

* fix: kv and perms

* fix: driver registrations

* fix: home mapping

* fix: rao

* adding back 500 alarm

* fix: build paths

* fix: fs and kv shapes

* fix: kv shape

* more kv coercing and ai chat matching format as prior

* fix:  private app gates

* private app caches

* fix: whole bunch of legacy shape issues

* update template jsonc

* fix caching partial oidc and fs signed paths

* more oidc fixes

* fix: wip

* fix: private apps

* admin route fixes

* fix: last few things hopefully

* claude uploads

* fix security for app only routes

* fix kv system namespace

* stuff

* fix: app and kv and suggested apps

* fix:open item

* fix: FS operations

* fix: default app icons

* add back token-read and WSL support

* metering fixes

* fix: fsEntry

* perm scanners and implicators

* proper download endpoint

* fix: download

* fix anti csrft on v2

* fix file extensions, app icons

* fold in v1 fixes from origin/main into v2 equivalents

Re-applies the v1 fixes that landed on origin/main into their v2
counterparts since the v1 files were deleted on DS/wip during the v2
migration. v1 commits referenced below.

- SQLBatcher: flush immediately when queue hits maxBatchSize instead
  of racing the timer (v1 12f48238).
- RedisClient: drop maxRetriesPerRequest from 2 to 1 to shrink failure
  window (v1 b6776ab4).
- ChatCompletionDriver: default minimumCredits to 1 when unset/zero so
  zero-cost precheck doesn't auto-pass (v1 36bd6073).
- OpenAiImageProvider: add gpt-image-2 support — open-ended size rules,
  token-based cost estimator, arbitrary-size normalizer, isGpt prefix
  broadened to gpt-image- (v1 f14f1bf4). models.ts auto-merged via
  rename detection.
- AppStore: bump row cache TTL from 5m to 24h (v1 6b3196ed).

Not ported: v1 app-object Redis cache (bdfa12b5/b886dde3) — v2's
#toClient recomputes filetype_associations/created_from_origin per
read; adding a second cache layer is a larger change for a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* remoe anti-csrf from auth routes that had not used them

* more icon fixes

* fix worker functionality

* fix: app and subdomain es

Co-authored-by: Copilot <[email protected]>

* fix PUT-761

* fix: PUT-748

* fix: rename fsService

* Add security back to WorkerDriver

* Migrate worker from fsEntry to fs. Fix cache issue

* remove ability to create symlinks

* strict webdav acl

* require auth for wisp

* chore: service renames

* Add metering back to puter peer api

* fix: PUT-760 PUT-749

* fix: PUT-746

* fix: peer cost

Co-authored-by: Copilot <[email protected]>

* fix: 771

* change order of peer controller

* fix: create appdata folder for app on get auth token

* fix: align delete site and list sites

* delete: putility

* fix subdomains

* Add support for tilde in subdomains, fix subdomain update

* cleanup PeerController.ts and fix billing oversight (#2844)

* fix: PUT-786

* fix: bugs

* fix: issues with multiple subdomain queries, or permission checks

* fix: harden response shapes to not contain uneeded fields

* fix: move state to redis

* fix: missing kv methods + better sec

Co-authored-by: Copilot <[email protected]>

* fix: subdomainStore limit

* fix: missing path resolution

Co-authored-by: Copilot <[email protected]>

* fs fixes

* fix: undef error

* fix fs + cleanup

* fix: npm audit fixes

* heal path entries where missing

Co-authored-by: Copilot <[email protected]>

* fix: caching

Co-authored-by: Copilot <[email protected]>

* fix: cache inconsistencies

Co-authored-by: Copilot <[email protected]>

* fix: app driver metadata

Co-authored-by: Copilot <[email protected]>

* remove extraneous comma

* fix: associated app icons

* fix: bad tool call

* Add validation to WorkerDriver#getFilePaths

* misc fs and auth issues

Co-authored-by: Copilot <[email protected]>

* fix: oidc errors

Co-authored-by: Copilot <[email protected]>

* fix: PUT-797

* fix: legacy appdata_app

Co-authored-by: Copilot <[email protected]>

* fix: add alert logs

Co-authored-by: Copilot <[email protected]>

* fix: error handling

* Disable sharecontroller

* fix: remove private user identifier for ai

* fix: private app fixes

* Add backback signup_server

* fix: completionId size

Co-authored-by: Copilot <[email protected]>

* fix: revalidate path for oidc

* fix: revalidate path for oidc

* fix: email validation

Co-authored-by: Copilot <[email protected]>

* fix: user create query

* fix: middleware extensions

Co-authored-by: Copilot <[email protected]>

* use x-forwarded-for for req ip forwarded

* fix: missing last_activity ts

* feat: add cache broadcast to subdomains

* fix: update config typing

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: ProgrammerIn-wonderland <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Nariman Jelveh <[email protected]>
Co-authored-by: velzie <[email protected]>
2026-04-30 12:13:43 -07:00
Miika Kuisma b593eb5630 Tests update 2026-04-29 16:45:09 +03:00
1815 changed files with 184375 additions and 161015 deletions

No files matched your search

+36
View File
@@ -1,4 +1,40 @@
.dockerignore
Dockerfile
docker-compose.yml
# Dev / build artifacts (recreated inside the build stage)
node_modules
dist
src/gui/dist
src/puter-js/dist
*.tsbuildinfo
# Local runtime data
volatile
config.json
config.dev.json
/puter
# OS / editor
.DS_Store
.vscode
.idea
# Git / CI
.git
.github
# Logs
*.log
npm-debug.log*
.npm
# Tests / coverage
coverage
.nyc_output
# Secrets
.env
.env.*
creds*
*.pem
+18 -1
View File
@@ -1 +1,18 @@
PORT=4000
# Copy this file to `.env`, fill in the secrets, and `docker compose -f
# docker-compose.full.yml up -d`. None of the defaults below are safe for
# anything beyond a local laptop test.
# ── Public-facing ports (nginx) ---------------------------------------
HTTP_PORT=80
# HTTPS_PORT=443 # uncomment after you enable TLS in nginx/nginx.conf
# ── MariaDB ------------------------------------------------------------
MARIADB_ROOT_PASSWORD=replace-with-strong-password
MARIADB_DATABASE=puter
MARIADB_USER=puter
MARIADB_PASSWORD=replace-with-strong-password
# ── S3 (RustFS) --------------------------------------------------------
S3_ACCESS_KEY=puter
S3_SECRET_KEY=replace-with-strong-secret
S3_BUCKET=puter-local
@@ -0,0 +1,49 @@
name: AI Provider Integration Tests
# Hits real provider APIs with cheap models, so this workflow is
# manually triggered only. Each provider's test reads its credential
# from PUTER_TEST_AI_<PROVIDER>_API_KEY and skips itself silently if
# the secret is missing.
on:
workflow_dispatch:
permissions:
contents: read
jobs:
ai-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run AI provider integration tests
# Vitest treats positional args as filename substring filters.
# The pattern matches every `*.integration.test.ts` file and
# nothing else — the regular backend test suite stays out.
run: npm run test:backend -- integration.test
env:
CI: 'true'
PUTER_TEST_AI_CLAUDE_API_KEY: ${{ secrets.PUTER_TEST_AI_CLAUDE_API_KEY }}
PUTER_TEST_AI_OPENAI_API_KEY: ${{ secrets.PUTER_TEST_AI_OPENAI_API_KEY }}
PUTER_TEST_AI_GEMINI_API_KEY: ${{ secrets.PUTER_TEST_AI_GEMINI_API_KEY }}
PUTER_TEST_AI_GROQ_API_KEY: ${{ secrets.PUTER_TEST_AI_GROQ_API_KEY }}
PUTER_TEST_AI_MISTRAL_API_KEY: ${{ secrets.PUTER_TEST_AI_MISTRAL_API_KEY }}
PUTER_TEST_AI_DEEPSEEK_API_KEY: ${{ secrets.PUTER_TEST_AI_DEEPSEEK_API_KEY }}
PUTER_TEST_AI_XAI_API_KEY: ${{ secrets.PUTER_TEST_AI_XAI_API_KEY }}
PUTER_TEST_AI_OPENROUTER_API_KEY: ${{ secrets.PUTER_TEST_AI_OPENROUTER_API_KEY }}
PUTER_TEST_AI_TOGETHER_API_KEY: ${{ secrets.PUTER_TEST_AI_TOGETHER_API_KEY }}
PUTER_TEST_AI_MOONSHOT_API_KEY: ${{ secrets.PUTER_TEST_AI_MOONSHOT_API_KEY }}
PUTER_TEST_AI_ZAI_API_KEY: ${{ secrets.PUTER_TEST_AI_ZAI_API_KEY }}
PUTER_TEST_AI_ELEVENLABS_API_KEY: ${{ secrets.PUTER_TEST_AI_ELEVENLABS_API_KEY }}
+79
View File
@@ -0,0 +1,79 @@
name: Backend Tests
on:
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- 'src/docs/**'
permissions:
contents: read
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- ref: ${{ github.base_ref }}
artifact: base
- ref: ${{ github.head_ref }}
artifact: pr
steps:
- name: Checkout ${{ matrix.ref }}
uses: actions/checkout@v4
with:
ref: ${{ matrix.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run backend tests with coverage
run: npm run test:backend -- --coverage
env:
CI: 'true'
- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: backend-coverage-${{ matrix.artifact }}
path: src/backend/coverage/
retention-days: 14
report-coverage:
needs: test
if: always()
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download PR coverage
uses: actions/download-artifact@v4
with:
name: backend-coverage-pr
path: src/backend/coverage
- name: Download base coverage
uses: actions/download-artifact@v4
with:
name: backend-coverage-base
path: coverage-base
- name: Report coverage on PR
uses: davelosert/vitest-coverage-report-action@v2
with:
json-summary-path: src/backend/coverage/coverage-summary.json
json-final-path: src/backend/coverage/coverage-final.json
json-summary-compare-path: coverage-base/coverage-summary.json
+87 -70
View File
@@ -1,84 +1,101 @@
#
name: Docker Image CI
# Configures this workflow to run every time a change is pushed to the
# branch called `main`.
# Two ways in:
# - push of a calver tag (YY.MM or YY.MM.p) → publishes <version>, latest, main
# - workflow_dispatch → publishes latest + main from whichever ref the user
# picks in the Actions UI. Used to fast-forward :latest/:main when the
# branch has moved ahead of the most recent release tag.
on:
push:
tags:
- '*.*.*'
branches:
- 'main'
push:
tags:
- '[0-9][0-9].[0-9][0-9]'
- '[0-9][0-9].[0-9][0-9].[0-9]*'
workflow_dispatch:
inputs:
push_latest:
description: 'Push :latest tag'
type: boolean
default: true
push_main:
description: 'Push :main tag'
type: boolean
default: true
# Defines two custom environment variables for the workflow. These are used
# for the Container registry domain, and a name for the Docker image that
# this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the
# latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions
# in this job.
permissions:
contents: read
packages: write
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
steps:
- name: Validate calver tag
if: github.event_name == 'push'
env:
REF_NAME: ${{ github.ref_name }}
run: |
if ! [[ "$REF_NAME" =~ ^[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
echo "Tag '$REF_NAME' does not match YY.MM or YY.MM.p calver format."
exit 1
fi
# Uses the `docker/login-action` action to log in to the Container
# registry using the account and password that will publish the packages.
# Once published, the packages are scoped to the account defined here.
- name: Log in to GitHub Package Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Log in to GitHub Package Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about)
# to extract tags and labels that will be applied to the specified image.
# The `id` "meta" allows the output of this step to be referenced in
# a subsequent step. The `images` value provides the base name for the
# tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# This step uses the `docker/build-push-action` action to build the
# image, based on your repository's `Dockerfile`. If the build succeeds,
# it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the
# set of files located in the specified path. For more information, see
# "[Usage](https://github.com/docker/build-push-action#usage)" in the
# README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image
# with the output from the "meta" step.
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
heyputer/puter
# type=ref,event=tag only emits on tag pushes, so manual runs skip
# the calver tag and just publish whichever of latest / main the
# dispatch inputs asked for. On a tag push, both default to true.
tags: |
type=ref,event=tag
type=raw,value=latest,enable=${{ github.event_name == 'push' || inputs.push_latest }}
type=raw,value=main,enable=${{ github.event_name == 'push' || inputs.push_main }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Sync README to Docker Hub
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: heyputer/puter
readme-filepath: ./README.md
+27
View File
@@ -0,0 +1,27 @@
name: Docs Build
on:
pull_request:
paths:
- 'src/docs/**'
jobs:
build-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build docs
run: npm run build
working-directory: src/docs
+72
View File
@@ -0,0 +1,72 @@
name: Tag Alias
# Retags an existing image in GHCR under a custom name, without rebuilding.
# Uses `docker buildx imagetools create` so the multi-arch manifest is preserved.
# Re-running with the same name updates the alias to point at a different source tag.
on:
workflow_dispatch:
inputs:
name:
description: "Alias name (e.g., potato)"
required: true
source_tag:
description: "Source tag to alias from"
required: false
default: "latest"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
alias:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Validate alias name
env:
NAME: ${{ inputs.name }}
run: |
if ! [[ "$NAME" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then
echo "Invalid alias name '$NAME' (allowed: [A-Za-z0-9_.-], must start with [A-Za-z0-9_], max 128 chars)."
exit 1
fi
if [[ "$NAME" == "latest" || "$NAME" == "main" ]]; then
echo "Alias name '$NAME' is reserved by the build workflow."
exit 1
fi
if [[ "$NAME" =~ ^[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]]; then
echo "Alias name '$NAME' looks like a calver tag — pick a non-version name."
exit 1
fi
- name: Log in to GitHub Package Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create alias tag
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
NAME: ${{ inputs.name }}
SOURCE_TAG: ${{ inputs.source_tag }}
run: |
SRC="${IMAGE}:${SOURCE_TAG}"
DST="${IMAGE}:${NAME}"
echo "Aliasing $DST -> $SRC"
docker buildx imagetools create -t "$DST" "$SRC"
{
echo "### 🏷️ Tag Alias"
echo ""
echo "- Source: \`$SRC\`"
echo "- Alias: \`$DST\`"
} >> "$GITHUB_STEP_SUMMARY"
-94
View File
@@ -1,94 +0,0 @@
name: test
on:
push:
branches: ["main"]
paths-ignore:
- "src/docs/**"
pull_request:
branches: ["main"]
paths-ignore:
- "src/docs/**"
jobs:
test-backend:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Backend Tests (with coverage)
env:
NODE_OPTIONS: "--max-old-space-size=4096"
run: |
npm ci
npm run build
npm run test:backend -- --coverage --coverage.reporter=json --coverage.reporter=json-summary --coverage.reporter=lcov
- name: Upload backend coverage report
if: ${{ always() && hashFiles('coverage/**/coverage-summary.json') != '' }}
uses: actions/upload-artifact@v4
with:
name: backend-coverage-${{ matrix.node-version }}
path: coverage
retention-days: 5
api-test:
name: API tests (node env, api-test)
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version: [24.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: API Test
run: |
pip install -r ./tests/ci/requirements.txt
./tests/ci/api-test.py
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: (api-test) server-logs
path: /tmp/backend.log
retention-days: 3
vitest:
name: puterjs (node env, vitest)
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version: [24.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Vitest Test
run: |
pip install -r ./tests/ci/requirements.txt
./tests/ci/vitest.py
+9
View File
@@ -22,6 +22,11 @@ dist/
.env
!.env.example
# Per-developer / deployment config override. `config.default.json` ships in
# the repo and is the authoritative source; `config.json` (if present) wins
# at runtime for the local machine only.
config.json
# this is for jetbrain IDEs
.idea/
/puter
@@ -63,3 +68,7 @@ AGENTS.md
coverage/
*.log
undefined
servers.json
config.*.json
volatile/
-12
View File
@@ -1,12 +0,0 @@
[submodule "submodules/v86"]
path = submodules/v86
url = [email protected]:HeyPuter/v86.git
[submodule "submodules/twisp"]
path = submodules/twisp
url = [email protected]:MercuryWorkshop/twisp.git
[submodule "submodules/epoxy-tls"]
path = submodules/epoxy-tls
url = [email protected]:MercuryWorkshop/epoxy-tls.git
[submodule "submodules/wiki"]
path = submodules/wiki
url = https://github.com/HeyPuter/puter.wiki.git
Regular → Executable
+10 -8
View File
@@ -1,13 +1,15 @@
#!/usr/bin/env sh
tmpfile="$(mktemp)"
git diff --cached --name-only -z --diff-filter=ACMR -- \
'*.js' '*.mjs' '*.cjs' '*.jsx' '*.ts' '*.tsx' '*.vue' \
> "$tmpfile"
files=$(git diff --cached --name-only --diff-filter=ACMR | \
grep -E '^(src/backend|extensions)/.*\.(js|mjs|cjs|ts)$' || true)
if [ -s "$tmpfile" ]; then
xargs -0 eslint --fix < "$tmpfile" || true
xargs -0 git add < "$tmpfile"
if [ -z "$files" ]; then
exit 0
fi
rm -f "$tmpfile"
echo "$files" | xargs npx eslint --fix --no-warn-ignored
status=$?
echo "$files" | xargs git add
exit $status
-57
View File
@@ -1,57 +0,0 @@
# To learn more about how to use Nix to configure your environment
# see: https://developers.google.com/idx/guides/customize-idx-env
{ pkgs, ... }: {
# Which nixpkgs channel to use.
channel = "stable-25.05"; # or "unstable"
# Use https://search.nixos.org/packages to find packages
packages = [
pkgs.python3
pkgs.nodejs_24
];
# Sets environment variables in the workspace
env = {};
idx = {
# Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
extensions = [
# "vscodevim.vim"
];
# Enable previews and customize configuration
previews = {
# Currently disabled because the preview system wasn't working
enable = false;
previews = {
web = {
command = [
"npm"
"run"
"start"
"--"
"--port"
"$PORT"
"--host"
"0.0.0.0"
"--disable-host-check"
];
manager = "web";
};
};
};
# Workspace lifecycle hooks
workspace = {
# Runs when a workspace is first created
onCreate = {
# npm-install = "npm install";
# Currently disabled because the preview system wasn't working
};
# Runs when the workspace is (re)started
onStart = {
# npm-install = "npm install";
# Currently disabled because the preview system wasn't working
};
};
};
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

View File
Whitespace-only changes.
+2 -1
View File
@@ -1,3 +1,4 @@
node_modules
dist
build
build
**/*.dbmig.js
+4
View File
@@ -0,0 +1,4 @@
{
"tabWidth": 4,
"singleQuote": true
}
+101
View File
@@ -0,0 +1,101 @@
# AGENTS.md
Guidance for AI coding agents working in this repository. Read this before making changes. FOLLOW GUIDANCE AS CLOSELY AS POSSIBLE. If you think the guidance is wrong, raise an issue or flag a maintainer — don't just do what you think is right. This is the source of truth for how we write code, tests, and docs in this repo.
## Documentation Index
Use these as the source of truth before exploring further:
- [README.md](README.md) — project overview and quickstart.
- [doc/architecture.md](doc/architecture.md) — backend layered stack (controllers → drivers → services → stores → clients), `PuterServer` wiring, `Context` (ALS), and extensions.
- [doc/self-hosting.md](doc/self-hosting.md) — running Puter outside hosted infra.
- [CONTRIBUTING.md](CONTRIBUTING.md) — testing, security, AI-assisted code, PR conventions, Boy Scout Rule.
- [SECURITY.md](SECURITY.md) — how to report vulnerabilities (do not file them publicly).
- [BUG-BOUNTY.md](BUG-BOUNTY.md) — bounty program scope.
- [TRADEMARK.md](TRADEMARK.md) — trademark usage.
---
## Backend
The backend is organized as a layered stack inspired by Controller–Service–Repository with dependency injection. Every layer only depends on the layers beneath it, and `PuterServer` ([src/backend/server.ts](src/backend/server.ts)) wires the whole thing together.
### Layers (top → bottom)
| Layer | Lives in | Responsibility |
| --------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Controllers** | [src/backend/controllers/](src/backend/controllers/) | Route handlers. Parse + validate input, apply per-route gates (auth, subdomain, rate limit, body parsers via `RouteOptions`), call services, format responses. |
| **Drivers** | [src/backend/drivers/](src/backend/drivers/) | Optional. RPC-style handlers exposed over `/drivers/*`. Thin shells that validate RPC inputs and call into services/stores. |
| **Services** | [src/backend/services/](src/backend/services/) | Business logic. Assume the caller is already authenticated/authorized — services do not run auth gates themselves. |
| **Stores** | [src/backend/stores/](src/backend/stores/) | Persistence. Wraps clients with the domain shape services consume (rows, entities, KV namespaces). |
| **Clients** | [src/backend/clients/](src/backend/clients/) | Adapters for external/internal services (sql, redis, s3, dynamo, email, event bus). Knows protocols, not domain concepts. |
| **Config** | `config.*.json` → `IConfig` | Flat, typed config object every layer receives at construction. |
Each layer receives the layers beneath it through its constructor. Dependencies are explicit and traceable from `PuterServer`.
### Cross-layer rules
- **Don't reach across layers.** Controllers do not poke clients directly; services do not register routes. If you want to, the abstraction is wrong — fix the abstraction.
- **Don't call sideways within a layer for code reuse.** If two services need the same logic, lift it into a util/helper. Services should not depend on other services for shared code.
- **Prefer explicit arguments over `Context` (ALS).** Reach for [Context](src/backend/core/context.ts) only when the value is genuinely request-scoped and would otherwise thread through many layers. Today it's used sparingly — mostly for `actor` and `req`.
### Extensions
Extensions live in [extensions/](extensions/) and parallel the layered stack. They are for **non-crucial parts of the system** — things Puter still works without if removed. If a feature is load-bearing for clients, it belongs in core, not in an extension (see [whoami](extensions/whoami.ts) as the cautionary example).
The `extension` global ([src/backend/extensions.ts](src/backend/extensions.ts)) exposes:
- `extension.registerClient/Store/Service/Driver/Controller(name, Class)` for first-class additions.
- `extension.on(event, handler)` and `extension.get/post/put/delete/patch/use(path, opts?, handler)` for the lightweight common case. `opts` is the same `RouteOptions` controllers use.
- `extension.import('client' | 'store' | 'service' | 'controller' | 'driver')` returns a lazy proxy to instantiated objects. `extension.config` exposes live config.
### Language & file conventions
- **Modules:** We transpile and build as needed — write ES modules, not CommonJS.
- **TypeScript preferred for new files.** Existing JS is fine; convert opportunistically when you're already touching a file.
- **Reuse types and code before adding duplicates.** Before defining a type, search for an existing one; extend it if close. Before writing a helper or repeating logic, search for an existing utility, helper, or implementation and reuse or extend it. Only add a new type or helper when nothing suitable exists.
- **Make new types findable.** Co-locate them with the layer/module that owns them, export from the obvious entry point, and use a descriptive `PascalCase` name. Don't hide types in random files where future readers won't grep them.
- **Naming:** `camelCase` for variables/functions, `PascalCase` for classes and for files containing a class (`AuthService.ts`, `KVStoreDriver.ts`).
### Comments
Keep comments light. Prefer self-documenting code — clear names, small functions, obvious flow. Add a comment **only** when:
- The _why_ is non-obvious (a hidden constraint, a workaround, a subtle invariant).
- A non-trivial usage detail would otherwise trip the next reader.
If comments need to be more than a line, its probably too long, and when it does need multiline comments, use multiline `/** ... */` JSDoc style for consistency. Use `//` for single-line comments.
Don't restate what the code already says. Don't write comments that reference the current task or PR or version of something — those rot.
**No ticket references in code or test comments.** Don't write `PUT-1234`, `AUTH-3`, `GUI-2`, `// fix for FOO-99`, `// per ROLLOUT-1`, or section headers like `// -- PUT-1020: ... --`. The ticket exists in git history and the issue tracker; the code should describe the _why_ in domain terms ("logout cascade transparently invalidates...") not project-management terms ("the cascade from AUTH-3..."). Same rule for test-block names and section dividers — describe what's being tested, not which ticket asked for it.
**Use plain ASCII `-` in comment section dividers, not box-drawing characters.** Write `// -- Section name --`, not `// ── Section name ──`. The `─` (U+2500) character renders inconsistently in some editors and diff tools and adds nothing over `-`.
### Tests
When adding new behavior (function, endpoint, driver method, branch of logic), add a test for it.
- Use Vitest for unit and integration tests. Test files go next to the code they test, named `*.test.ts` or `*.test.js`.
- **Mock data, not methods.** Stub the inputs (fixtures, fake rows, sample payloads) rather than mocking the function under test or the layer beneath it. Over-mocking produces tests that pass while production breaks. If you must mock, mock at a real boundary (a client/external service), not within the same layer you're testing.
- Prefer test server over mocking deps. We have existing test utilities for running full test server to boot deps based on config. (see [src/backend/testUtil.ts](src/backend/testUtil.ts)).
- **Hit a real database / real client where reasonable.** Integration shapes catch the things unit tests with mocks miss.
- **Regression tests for bug fixes.** A test that fails before your fix and passes after is the cheapest insurance against the bug coming back.
- If something is genuinely hard to test (UI animation, third-party glue), skip it but say so in the PR.
### Security & privacy
Before opening a PR, scan the diff for:
- Logs, error messages, or responses leaking internal paths, secrets, tokens, env vars, or other users' data.
- Debug routes, test credentials, commented-out auth checks.
- Endpoints returning more than the caller actually needs.
When in doubt, return less. Auth-, permission-, or data-export-related changes deserve an explicit callout in the PR description.
### Working rules of thumb
- **Run it, don't just compile it.** "It type-checks" is not "it works." Exercise the code path end-to-end at least once.
- **Read the neighbors before writing.** Match the shape of similar things already in the tree. If you genuinely think the existing pattern is wrong, raise it — don't quietly diverge.
- **Boy Scout Rule, proportional to the change.** Fix the obvious typo, dead import, or missing typehint in files you're already touching. Don't ride a refactor along with a bug fix — that just makes review harder.
- **Understand what you commit.** AI assistance is fine; shipping code you couldn't defend in review is not.
+35 -2
View File
@@ -6,7 +6,7 @@ We at **Puter** are committed to maintaining a secure experience for our users a
The following are in scope for this program:
* **The Puter open-source project** (available at [github.com/HeyPuter](https://github.com/HeyPuter/puter))
* **The Puter open-source project** (available at [github.com/HeyPuter/puter](https://github.com/HeyPuter/puter))
* **`puter.com`**
* **`api.puter.com`**
@@ -17,18 +17,51 @@ Out-of-scope:
* Denial of Service (DoS), spam, or volumetric attacks.
* Physical security issues.
## Known Non-Issues (Please Check Before Submitting)
The following have already been reviewed and determined **not to be vulnerabilities**. Reports that only re-describe one of these are **not eligible for a reward** and will be closed as non-issues — even if they include new code references. Please review this list before submitting:
* **XSS / CORS / token issues scoped to `api.puter.com`.** The primary user session cookie lives on `puter.com`, not on the API origin, so reflected/stored XSS, CORS, or token handling on `api.puter.com` is generally out of scope. Two exceptions we *do* evaluate on their merits — please report these: (a) attacker-controlled content served **inline** (e.g. an HTML `Content-Type`) from API file/response endpoints, and (b) anything that abuses the app-scoped `puter_token_v2` companion cookie set on the API host.
* **SSRF via `secureFetch`.** Production routes outbound requests through an isolated proxy that has no access to internal/SSRF-sensitive resources.
* **Attacks that depend on guessing an `appInstanceID` or app UID.** These are random 128-bit secret values and are not considered guessable.
* **Apps invoking drivers, creating workers, or using KV.** Applications are intended to do this; worker permissions are scoped to the owning app. This is by design.
* **App metadata or app user-count "leaks".** This information is currently public by design.
* **General "token in a URL" / token-lifetime designs** — signed directory URLs exposing children, a write signature implying read, or app tokens outliving a web session. These are current intended behaviors.
* **Missing PKCE, unverified `id_token` signatures, or other OIDC hardening.** Provider tokens are obtained through a server-to-server authorization-code exchange with the provider's token endpoint over TLS, so the resulting `id_token` / userinfo claims are trusted from that channel rather than from local JWKS signature verification — the callback never accepts a caller-supplied `id_token`. Adding local signature / `aud` / `iss` / `exp` checks is welcome defense-in-depth (please open a GitHub issue/PR), but their absence is not an account-takeover vector on its own.
* **JWT "algorithm confusion" / unpinned `algorithms` in `jwt.verify`.** Puter's session tokens are HMAC-signed (HS256) with a server-side secret, and there is no asymmetric public key anywhere in the verification path, so `alg`-substitution attacks do not apply. Explicitly pinning `algorithms` is a fine hardening PR, but it is not a vulnerability.
* **Static-source "SQL injection" in internal pagination.** Findings such as `LIMIT ${limit}` in list/notification queries: the limit is numerically coerced and clamped before it reaches the query, so it is not reachable with attacker-controlled string input. Hardening PRs to the internal stores are welcome, but these are not exploitable as reported.
* **Deprecated `saveTo*` GUI app messages.** The legacy `saveToDesktop` / `saveToDocuments` / etc. app-IPC handlers can create — never overwrite — new files in standard user folders. The behavior is non-destructive, path-traversal-safe, and deprecated (slated for removal); it is not treated as a sandbox escape.
* **Best-practice suggestions** such as login/registration username enumeration (kept intentionally for UX) or unauthenticated unsubscribe links (industry norm).
* **Rate-limiting suggestions for TURN credential issuance** (intentional; not billed per tunnel).
If you believe you have a **genuinely new** exploit chain that defeats one of these rationales (for example, demonstrating a sensitive credential that really is reachable on `api.puter.com`), say so explicitly and show why the reasoning above does not apply.
## Rules of Engagement
To participate, you must:
1. **Report responsibly**: Provide detailed steps to reproduce the issue, including proof-of-concept code or screenshots where applicable.
1. **Report responsibly**: Provide detailed steps to reproduce the issue, including proof-of-concept code, screenshots, or a screen recording (see *Proof of Reproduction* below).
2. **Do no harm**: Do not exfiltrate, modify, or delete data. Only access your own account or test data.
3. **Respect availability**: Do not perform denial-of-service attacks or automated scans that degrade service.
4. **Follow disclosure policy**: Do not publicly disclose vulnerabilities until we have confirmed and patched the issue.
5. **Act in good faith**: Make every effort to avoid privacy violations, destruction of data, and interruption or degradation of services.
6. **Check the Known Non-Issues list**: Reports matching an item in the "Known Non-Issues" section above are not eligible and will be closed.
Reports that do not meet these guidelines may not be eligible for a reward.
## Proof of Reproduction
Reports must demonstrate a **working, reproducible exploit with real impact** — not a theoretical or static-source-review finding. Please include:
* Exact steps to reproduce, the relevant request/response or code path, and the commit or version you tested.
* The **observed** result versus the **expected** result.
* For client-side, UI, or authentication-flow bugs: a short screen recording (≤ 2 minutes) showing the exploit working end-to-end on a real Puter instance.
* For server-side bugs: a runnable proof-of-concept.
Reports based solely on reading the source ("source review only, not tested") or unverified AI/LLM-generated reports are the **lowest triage priority and are generally not eligible**. If you used an AI tool to help find an issue, you must personally verify that it actually reproduces before submitting.
Please submit **one issue per report**. Bundled "audit packs" of many speculative findings will be declined; send each confirmed issue separately.
## Reporting Process
To report a vulnerability, email us at: **[[email protected]](mailto:[email protected])**.
+1
View File
@@ -0,0 +1 @@
FOLLOW ./AGENTS.md
+27 -55
View File
@@ -1,78 +1,50 @@
# Contributing to Puter
# Contributing to Puter Backend
Welcome to Puter, the open-source distributed internet operating system. We're excited to have you contribute to our project, whether you're reporting bugs, suggesting new features, or contributing code. This guide will help you get started with contributing to Puter in different ways.
Thanks for contributing. These rules aren't strictly enforced — but following them makes every PR easier. If anything's unclear, ping a core maintainer or open the PR and ask.
<br>
New to the backend? Start with [doc/architecture.md](doc/architecture.md).
# Report bugs
---
Before reporting a bug, please check [the issues on our GitHub repository](https://github.com/HeyPuter/puter/issues) to see if the bug has already been reported. If it has, you can add a comment to the existing issue with any additional information you have.
## 1. Test it. Run it.
If you find a new bug in Puter, please [open an issue on our GitHub repository](https://github.com/HeyPuter/puter/issues/new). We'll do our best to address the issue as soon as possible. When reporting a bug, please include as much information as possible, including:
Run the affected code path end-to-end before opening a PR. "It builds" is not "it works."
- A clear and descriptive title
- A description of the issue
- Steps to reproduce the bug
- Expected behavior
- Actual behavior
- Screenshots, if applicable
- Your host operating system and browser
- Your Puter version, location, ...
Add tests for new behavior, endpoints, or bug fixes. If something's genuinely hard to test, say so in the PR.
Please open a separate issue for each bug you find.
## 2. Follow existing patterns
Maintainers will apply the appropriate labels to your issue.
Match the shape of similar code already in the repo. [doc/architecture.md](doc/architecture.md) is the source of truth for layers, wiring, and naming. If you think a pattern is wrong, raise it — don't quietly diverge.
<br>
## 3. Don't expose system or user information
# Suggest new features
Scan your diff for stray logs, debug routes, internal paths, secrets, tokens, or user data in errors/responses. When in doubt, return less. Flag any auth, permission, or data-export changes in the PR description.
If you have an idea for a new feature in Puter, please open a new discussion thread on our [GitHub repository](https://github.com/HeyPuter/puter/discussions) to discuss your idea with the community. We'll do our best to respond to your suggestion as soon as possible.
For private security reports, see [SECURITY.md](SECURITY.md).
When suggesting a new feature, please include as much information as possible, including:
## 4. AI-assisted code is fine — understood code is required
- A clear and descriptive title
- A description of the feature
- The problem the feature will solve
- Any relevant screenshots or mockups
- Any relevant links or resources
Don't commit code you couldn't have written, debugged, or defended yourself. Read the diff, run it, and be ready to explain it in review.
<br>
## 5. Update docs for API changes
# Contribute code
If you change puter-js APIs (drivers or endpoints used by puter-js), update [developer docs](src/docs/). puter-js itself may also need updating.
If you'd like to contribute code to Puter, you need to fork the project and submit a pull request. If this is your first time contributing to an open-source project, we recommend reading this short guide by GitHub on [how to contribute to a project](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project).
## 6. Boy Scout Rule — leave it 1% better
We'll review your pull request and work with you to get your changes merged into the project.
![Boy Scout Rule](https://imgs.search.brave.com/DMmIWl5-NuZVtrR9kXBb06AKF8kturkgSW9UMb2-6m4/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly9sYXdz/b2Zzb2Z0d2FyZWVu/Z2luZWVyaW5nLmNv/bS9pbWFnZXMvbGF3/cy9ib3ktc2NvdXQt/cnVsZS5wbmc)
<br>
Fix the typo, the dead import, the missing test, the bit you had to read twice. Keep cleanup proportional to the change — no refactors riding along on bug fixes.
## PR Standards
---
We expect the following from pull requests (it makes things easier):
- If you're closing an issue, please reference that issue in the PR description
- Avoid whitespace changes
- No regressions for "appspace" (Puter apps)
## Opening a PR
<br>
- One thing per PR where possible.
- Describe **what** and **why**; the diff shows how.
- Mention how you tested user-visible changes.
- Drafts welcome.
## Code Review
---
Once you've submitted your pull request, the project maintainers will review your changes. We may suggest some changes or improvements. This is a normal part of the process, and your contributions are greatly appreciated!
<br>
## Contribution License Agreement (CLA)
Like many open source projects, we require contributors to sign a Contribution License Agreement (CLA) before we can accept your code. When you open a pull request for the first time, a bot will automatically add a comment with a link to the CLA. You can sign the CLA electronically by following the link and filling out the form.
<br>
# Getting Help
If you have any questions about Puter, please feel free to reach out to us through the following channels:
- [Discord](https://discord.com/invite/PQcx7Teh8u)
- [Reddit](https://www.reddit.com/r/Puter/)
- [Twitter](https://twitter.com/HeyPuter)
- [Email](mailto:[email protected])
Questions? Message a core maintainer. Welcome aboard.
+67 -69
View File
@@ -1,90 +1,88 @@
# /!\ NOTICE /!\
# syntax=docker/dockerfile:1.7
#
# OSS Puter image — multi-arch (linux/amd64, linux/arm64).
#
# Build & push:
# docker buildx build --platform linux/amd64,linux/arm64 \
# -t ghcr.io/heyputer/puter:latest --push .
#
# Local single-arch build:
# docker build -t puter .
#
# Self-hosters inject configuration by mounting a config.json at
# /etc/puter/config.json. It is deep-merged over the bundled
# config.default.json, so partial overrides work. Absent file = defaults.
# Many of the developers DO NOT USE the Dockerfile or image.
# While we do test new changes to Docker configuration, it's
# possible that future changes to the repo might break it.
# When changing this file, please try to make it as resiliant
# to such changes as possible; developers shouldn't need to
# worry about Docker unless the build/run process changes.
# ---- Build stage ----
FROM node:24-slim AS build
# Build stage
FROM node:24-alpine AS build
WORKDIR /opt/puter
# Install build dependencies
RUN apk add --no-cache git python3 make g++ \
&& ln -sf /usr/bin/python3 /usr/bin/python
# Build toolchain needed for native deps (bcrypt, sharp, better-sqlite3, …).
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 make g++ git && \
rm -rf /var/lib/apt/lists/*
# Set up working directory
WORKDIR /app
ENV HUSKY=0
ENV npm_config_fund=false
ENV npm_config_audit=false
# Copy package.json and package-lock.json
# ---- Dependency layer ---------------------------------------------------
# Copy ONLY package manifests + lockfile first so the npm-install layer
# stays cached when only source files change.
COPY package.json package-lock.json ./
COPY src/backend/package.json src/backend/
COPY src/gui/package.json src/gui/
COPY src/puter-js/package.json src/puter-js/package-lock.json src/puter-js/
COPY src/worker/package.json src/worker/
COPY src/docs/package.json src/docs/
# Fail early if lockfile or manifest is missing
RUN test -f package.json && test -f package-lock.json
# extensionSetup.mjs runs as the postinstall hook during npm ci. (No-ops
# unless any packages/puter/extensions/* gain a package.json.)
COPY tools/extensionSetup.mjs tools/extensionSetup.mjs
# Copy the source files
RUN --mount=type=cache,target=/root/.npm \
npm ci
# ---- Source layer -------------------------------------------------------
COPY . .
# Install mocha
RUN npm i -g npm@latest
RUN npm install -g mocha
# Compile backend TS, then build GUI + puter-js webpack bundles in
# parallel. The GUI/puter-js bundles are how /dist/bundle.min.{js,css}
# and /sdk/puter.js fall back to local assets when the kernel-config
# CDN keys are unset.
RUN npm run build:ts
RUN set -e; \
(cd src/gui && node ./build.js) & gui_pid=$!; \
(cd src/puter-js && npm run build) & pjs_pid=$!; \
wait $gui_pid; \
wait $pjs_pid
# Install node modules
RUN npm cache clean --force && \
for i in 1 2 3; do \
npm ci && break || \
if [ $i -lt 3 ]; then \
sleep 15; \
else \
LOG_DIR="$(npm config get cache | tr -d '\"')/_logs"; \
echo "npm install failed; dumping logs from $LOG_DIR"; \
if [ -d "$LOG_DIR" ]; then \
ls -al "$LOG_DIR" || true; \
cat "$LOG_DIR"/* || true; \
else \
echo "Log directory not found (npm cache: $(npm config get cache))"; \
fi; \
exit 1; \
fi; \
done
# ---- Runtime stage (slim — no build tools) ----
FROM node:24-slim
# Run the build command if necessary
RUN cd src/gui && npm run build && cd -
WORKDIR /opt/puter
# Production stage
FROM node:24-alpine
# git: runtime version probe. wget: HEALTHCHECK.
RUN apt-get update && \
apt-get install -y --no-install-recommends git wget && \
rm -rf /var/lib/apt/lists/*
# Set labels
LABEL repo="https://github.com/HeyPuter/puter"
LABEL license="AGPL-3.0,https://github.com/HeyPuter/puter/blob/master/LICENSE.txt"
LABEL version="1.2.46-beta-1"
COPY --from=build --chown=node:node /opt/puter .
# Install git (required by Puter to check version)
RUN apk add --no-cache git
RUN mkdir -p /etc/puter /var/puter && \
chown -R node:node /etc/puter /var/puter
# Set up working directory
RUN mkdir -p /opt/puter/app
WORKDIR /opt/puter/app
# Copy built artifacts and necessary files from the build stage
COPY --from=build /app/src/gui/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY . .
# Set permissions
RUN chown -R node:node /opt/puter/app
USER node
# Self-hosters mount their override at this exact path. The v2 loader
# deep-merges it over config.default.json (see backend/index.ts).
ENV PUTER_CONFIG_PATH=/etc/puter/config.json
ENV NODE_OPTIONS=--enable-source-maps
EXPOSE 4100
HEALTHCHECK --interval=30s --timeout=3s \
USER node
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://puter.localhost:4100/test || exit 1
ENV NO_VAR_RUNTUME=1
# Attempt to fix `[email protected]` missing after build stage
# by doing a redundant `npm install` at this stage
RUN npm install
CMD ["npm", "start"]
CMD ["node", "-r", "./dist/src/backend/telemetry.js", "./dist/src/backend/index.js"]
+25 -68
View File
@@ -1,6 +1,6 @@
<h3 align="center"><img width="80" alt="Puter.com, The Personal Cloud Computer: All your files, apps, and games in one place accessible from anywhere at any time." src="https://assets.puter.site/puter-logo.png"></h3>
<h3 align="center">The Internet OS! Free, Open-Source, and Self-Hostable.</h3>
<h3 align="center">The Open-Source Internet Computer!</h3>
<p align="center">
<a href="https://puter.com/?ref=github.com"><strong>« LIVE DEMO »</strong></a>
@@ -12,8 +12,6 @@
·
<a href="https://developer.puter.com" target="_blank">Developers</a>
·
<a href="https://github.com/heyputer/puter-cli" target="_blank">CLI</a>
·
<a href="https://discord.com/invite/PQcx7Teh8u">Discord</a>
·
<a href="https://reddit.com/r/puter">Reddit</a>
@@ -21,19 +19,21 @@
<a href="https://twitter.com/HeyPuter">X</a>
</p>
<h3 align="center"><img width="800" style="border-radius:5px;" alt="screenshot" src="https://assets.puter.site/puter.com-screenshot-3.webp"></h3>
<h3 align="center"><img width="800" style="border-radius:5px;" alt="screenshot" src="https://assets.puter.site/puter.com-screenshot.webp"></h3>
<br/>
## Puter
Puter is an advanced, open-source internet operating system designed to be feature-rich, fast, and highly extensible. Puter can be used as:
Puter is an advanced, open-source, self-hostable internet computer designed to be feature-rich, fast, and highly extensible.
- A privacy-first personal cloud to keep all your files, apps, and games in one secure place, accessible from anywhere at any time.
- A platform for building and publishing websites, web apps, and games.
- An alternative to Dropbox, Google Drive, OneDrive, etc. with a fresh interface and powerful features.
- A remote desktop environment for servers and workstations.
- A friendly, open-source project and community to learn about web development, cloud computing, distributed systems, and much more!
### For Users
Puter's goal is to provide you with every app and feature you need to work, create, and play under one roof. From a simple [Notepad](https://online-notepad.com) and [Voice Recorder](https://voice-recorder.com) to [Spreadsheet](https://apps.puter.com/app/spreadsheet) and [Camera](https://online-camera.com), Puter wants to be the all-in-one solution for your digital life.
### For Developers
Puter provides everything you need to build and publish web apps and games. From [AI](https://developer.puter.com/ai/) to [Cloud Storage](https://developer.puter.com/object-storage/) and [Database](https://developer.puter.com/key-value-database/) to [Serverless Workers](https://developer.puter.com/serverless-workers/), Puter has you covered. Puter also helps you get users! Once you build your app, you can publish it on our [App Store](https://apps.puter.com/) to reach and monetize users.
<br/>
@@ -48,59 +48,26 @@ cd puter
npm install
npm start
```
**→** This should launch Puter at
<font color="red"> http://puter.localhost:4100 (or the next available port). </font>
**→** This should launch Puter at http://puter.localhost:4100
If this does not work, see [First Run Issues](./doc/self-hosters/first-run-issues.md) for
troubleshooting steps.
<br/>
### 🐳 Docker
```bash
mkdir puter && cd puter && mkdir -p puter/config puter/data && sudo chown -R 1000:1000 puter && docker run --rm -p 4100:4100 -v `pwd`/puter/config:/etc/puter -v `pwd`/puter/data:/var/puter ghcr.io/heyputer/puter
```
**→** This should launch Puter at
<font color="red"> http://puter.localhost:4100 (or the next available port). </font>
<br/>
### 🐙 Docker Compose
#### Linux/macOS
```bash
mkdir -p puter/config puter/data
sudo chown -R 1000:1000 puter
wget https://raw.githubusercontent.com/HeyPuter/puter/main/docker-compose.yml
docker compose up
```
**→** This should be available at
<font color="red"> http://puter.localhost:4100 (or the next available port). </font>
<br/>
#### Windows
```powershell
mkdir -p puter
cd puter
New-Item -Path "puter\config" -ItemType Directory -Force
New-Item -Path "puter\data" -ItemType Directory -Force
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/HeyPuter/puter/main/docker-compose.yml" -OutFile "docker-compose.yml"
docker compose up
```
**→** This should launch Puter at
<font color="red"> http://puter.localhost:4100 (or the next available port). </font>
<br/>
### 🚀 Self-Hosting
For detailed guides on self-hosting Puter, including configuration options and best practices, see our [Self-Hosting Documentation](https://github.com/HeyPuter/puter/blob/main/doc/self-hosters/instructions.md).
#### Linux/macOS
```bash
curl -fsSL https://puter.com/selfhost | sh
```
#### Windows
```powershell
irm https://puter.com/selfhost?os=windows | iex
```
**→** For more details, see [Self-Hosting Puter](./doc/self-hosting.md).
<br/>
@@ -110,16 +77,6 @@ Puter is available as a hosted service at [**puter.com**](https://puter.com).
<br/>
## System Requirements
- **Operating Systems:** Linux, macOS, Windows
- **RAM:** 2GB minimum (4GB recommended)
- **Disk Space:** 1GB free space
- **Node.js:** Version 24+
- **npm:** Latest stable version
<br/>
## Support
Connect with the maintainers and community through these channels:
@@ -129,7 +86,7 @@ Connect with the maintainers and community through these channels:
- X (Twitter): [x.com/HeyPuter](https://x.com/HeyPuter)
- Reddit: [reddit.com/r/puter/](https://www.reddit.com/r/puter/)
- Mastodon: [mastodon.social/@puter](https://mastodon.social/@puter)
- Security issues? [[email protected]](mailto:[email protected])
- Security issues or abuse reports? [[email protected]](mailto:[email protected])
- Email maintainers at [[email protected]](mailto:[email protected])
We are always happy to help you with any questions you may have. Don't hesitate to ask!
+46
View File
@@ -0,0 +1,46 @@
{
"config_name": "oss-default",
"env": "dev",
"port": 4100,
"protocol": "http",
"domain": "puter.localhost",
"cookie_name": "puter_auth_token",
"jwt_secret": "dev-jwt-secret-change-me",
"jwt_secret_v2": "dev-jwt-secret-v2-change-me",
"allow_v1_tokens": true,
"url_signature_secret": "dev-url-signature-secret-change-me",
"allow_all_host_values": true,
"allow_no_host_header": true,
"no_devwatch": false,
"enable_public_folders": true,
"is_storage_limited": false,
"min_pass_length": 6,
"static_hosting_domain": "site.puter.localhost",
"static_hosting_domain_alt": "host.puter.localhost",
"private_app_hosting_domain": "app.puter.localhost",
"private_app_hosting_domain_alt": "dev.puter.localhost",
"captcha": { "enabled": false },
"default_user_group": "78b1b1dd-c959-44d2-b02c-8735671f9997",
"default_temp_group": "b7220104-7905-4985-b996-649fdcdb3c8f",
"storage_capacity": 104857600,
"disable_user_signup": false,
"strict_email_verification_required": false,
"gui_assets_root": "./src/gui",
"puterjs_root": "./src/puter-js/dist",
"builtin_apps": {
"dev-center": "./src/dev-center"
},
"extensions": [
"./extensions"
],
"database": {
"engine": "sqlite",
"path": "volatile/runtime/puter-database.sqlite"
},
"s3": {
"localConfig": {
"dataDir": "volatile/runtime/fauxqs-data",
"s3StorageDir": "volatile/runtime/fauxqs-s3-data"
}
}
}
+387
View File
@@ -0,0 +1,387 @@
{
// Comprehensive template — every key the OSS backend or shipped (non-prod)
// extensions read. Copy to `config.json` and trim what you don't need;
// unset keys fall back to documented defaults. See
// `src/backend/types.ts` for the per-field source of truth.
//
// Each setting lives at exactly one canonical key — there are no fallback
// aliases. Values shown are illustrative, not production secrets.
//
// Keys consumed only by closed-source / hosted-prod extensions (clickhouse,
// cacheUpdateHandler, pages, prodMeteringAndBilling, …) are intentionally
// omitted.
// ── Environment / identity ──────────────────────────────────────────
"config_name": "template",
// `dev` opens a browser on boot, skips blocked-email checks, and runs the
// dev-time webpack watcher; `prod` serves pre-built bundles.
"env": "dev",
// Console output format. `json` replaces the global console so every call
// emits one structured JSON line (level, timestamp, msg, and the active
// request's trace id) — one event per call, so a line-oriented log
// collector can't split stack traces across events and level filtering
// works. Unset (the default) leaves console output human-readable.
"log_format": "text",
"version": "0.0.0",
// Stable identity for this server node — used by pager alerts and
// graceful-shutdown coordination.
"serverId": "node-1",
// ── Networking / URLs ───────────────────────────────────────────────
// Port Puter listens on internally.
"port": 4100,
// Externally-visible port (set this when behind a reverse proxy on 80/443).
"pub_port": 4100,
"protocol": "http",
"domain": "puter.localhost",
// Fully-qualified externally-visible URL. Computed from protocol/domain/
// pub_port if unset.
"origin": "http://puter.localhost:4100",
// Public base URL for the API subdomain (used to build signed URLs and
// surfaced to the client by the `installedApps` and `whoami` extensions).
"api_base_url": "http://api.puter.localhost:4100",
// Subdomains Puter routes on. Wildcard DNS (`*.<domain>`) must point at
// this server for site/app hosting to work.
"static_hosting_domain": "site.puter.localhost",
"static_hosting_domain_alt": "host.puter.localhost",
"private_app_hosting_domain": "app.puter.localhost",
"private_app_hosting_domain_alt": "dev.puter.localhost",
// Host-header / domain handling. Defaults below are the dev-friendly
// settings; tighten for any public install.
"allow_all_host_values": true,
"allow_no_host_header": true,
"allow_nipio_domains": false,
"custom_domains_enabled": false,
"enable_ip_validation": false,
// Express `trust proxy` setting — set to the number of reverse-proxy
// hops in front of the server (1 = nginx/Cloudflare, 2 = CF→ALB→app),
// or to a CIDR / IP / list. `false` (safe default) makes `req.ip` the
// direct socket peer. NEVER set to `true` in prod — it trusts every hop
// and makes X-Forwarded-For forgeable.
"trust_proxy": false,
"no_browser_launch": false,
// ── Dev watcher (devWatcher extension) ──────────────────────────────
// Rebuilds GUI + puter.js on file changes when running from source.
// Ignored when `env: "prod"` unless `devwatch.enabled: true`.
"no_devwatch": false,
"devwatch": {
// Delay after watcher startup before boot continues. Lets webpack
// emit its first build so the homepage doesn't 404 on bundle.min.js.
"ready_delay_ms": 5000
},
// ── Auth / session ──────────────────────────────────────────────────
// ALWAYS replace these for any public install — `openssl rand -hex 64`.
// `jwt_secret` is legacy verify-only; new tokens sign with `jwt_secret_v2`.
"jwt_secret": "change-me",
"jwt_secret_v2": "change-me",
// Set false to retire v1 tokens entirely (ROLLOUT-1).
"allow_v1_tokens": true,
"url_signature_secret": "change-me",
"cookie_name": "puter_auth_token",
"min_pass_length": 6,
// When true, anonymous users must log in instead of creating temp or
// permanent accounts.
"disable_user_signup": false,
"allow_system_login": false,
"strict_email_verification_required": false,
"captcha": {
"enabled": false,
"difficulty": "medium"
},
"oidc": {
"providers": {
// Google uses OIDC discovery — only ids are required.
"google": {
"client_id": "",
"client_secret": "",
"scopes": "openid email profile"
},
// Custom OIDC providers must also supply the three endpoints.
"custom-oidc": {
"client_id": "",
"client_secret": "",
"authorization_endpoint": "",
"token_endpoint": "",
"userinfo_endpoint": "",
"scopes": "openid email profile"
}
}
},
// ── Groups / provisioning ───────────────────────────────────────────
// UIDs of the persistent groups new users are auto-enrolled in.
"default_user_group": "78b1b1dd-c959-44d2-b02c-8735671f9997",
"default_temp_group": "b7220104-7905-4985-b996-649fdcdb3c8f",
// When true, ACL grants read/list on `/<user>/Public` to any actor.
"enable_public_folders": true,
// ── Storage / S3 ────────────────────────────────────────────────────
"s3": {
// Local fauxqs (in-process S3-compatible) — used in dev and the
// bundled-defaults Docker mode. Files land under `dataDir`.
"localConfig": {
"inMemory": false,
"host": "127.0.0.1",
"port": 4566,
"dataDir": "volatile/runtime/fauxqs-data",
"s3StorageDir": "volatile/runtime/fauxqs-s3-data"
},
// For real / external S3, replace `localConfig` above with `s3Config`.
"_remote_example": {
"s3Config": {
"useCredentialChain": false,
"endpoint": "https://s3.example.com",
// Endpoint used in presigned URLs handed to the browser. Set
// this when the server-side endpoint isn't reachable from the
// browser (e.g. docker-internal `http://s3:9000`).
"publicEndpoint": "",
"accessKeyId": "",
"secretAccessKey": "",
"region": "us-west-2",
// Set true for RustFS / MinIO / fauxqs (path-style URLs).
// Real AWS S3 wants virtual-hosted — leave unset / false.
"forcePathStyle": false
}
}
},
"s3_bucket": "puter-local",
"s3_region": "us-west-2",
"region": "us-west-2",
// Default per-user storage cap (bytes). 100 MB.
"storage_capacity": 104857600,
"is_storage_limited": false,
"available_device_storage": 0,
// ── Thumbnails (thumbnails extension) ───────────────────────────────
// Optional dedicated S3-compatible bucket for generated thumbnails.
// When unset (or `endpoint` empty), the extension falls back to the
// main S3 client / bucket above.
"thumbnailStore": {
"name": "puter-local",
"endpoint": "",
"credentials": {
"accessKeyId": "",
"secretAccessKey": ""
}
},
// ── Database ────────────────────────────────────────────────────────
"database": {
// `sqlite` for single-node/dev, `mysql` for MariaDB/MySQL,
// or `postgres` for PostgreSQL.
//
// NOTE: `postgres` support is a community contribution and is not
// exercised by Puter.com production. It boots, passes its own
// integration tests against pgmock, and runs the common user/app/
// fsentry/session/permission/OIDC flows — but less-traveled code
// paths may surface MySQL/SQLite-isms that haven't been ported yet.
// Expect rough edges and please file issues if you hit one. For
// production self-hosting today, `mysql` (MariaDB) and `sqlite` are
// the supported defaults.
"engine": "sqlite",
// sqlite — file path on disk
"path": "volatile/runtime/puter-database.sqlite",
"targetVersion": 0,
// mysql/postgres — connection details. PostgreSQL defaults to port
// 5432 when `engine` is `postgres`; MySQL/MariaDB normally use 3306.
"host": "",
"port": 3306,
"user": "",
"password": "",
"database": "",
// postgres may also use a URL instead of host/user/password fields:
// "connectionString": "postgres://puter:secret@localhost:5432/puter",
// Optional read-replica pool. Reads route here when populated.
"replica": {
"host": "",
"port": 3306,
"user": "",
"password": "",
"database": ""
}
// mysql/postgres self-host bootstrap: set `migrationPaths` to apply the
// bundled schema on first boot. Idempotent — safe to leave on.
// "migrationPaths": ["./src/backend/clients/database/migrations/mysql"]
// "migrationPaths": ["./src/backend/clients/database/migrations/postgres"]
},
// ── DynamoDB (KV store) ─────────────────────────────────────────────
"dynamo": {
// Local emulator (dynamodb-local) endpoint. Drop this field for real
// AWS DynamoDB.
"endpoint": "http://localhost:8000",
// Set true when pointing at a local emulator so Puter creates the KV
// table on boot. NEVER set against real AWS — provision via IaC.
// "bootstrapTables": true,
"path": "",
// Credentials. NOTE: snake_case here, unlike `s3.s3Config` below.
// For dynamodb-local, any non-empty values work.
"aws": {
"access_key": "",
"secret_key": "",
"region": "us-west-2"
}
},
// ── Redis / Valkey (cache + cross-node rate limit) ──────────────────
"redis": {
// True → in-process redis-mock (dev / single-node).
"useMock": true,
// Cluster nodes for ioredis. For a single Valkey/Redis container,
// run it in cluster mode (one node, all slots).
"startupNodes": [
{
"host": "127.0.0.1",
"port": 7000
}
]
// Defaults to true (matches prod ElastiCache). Set false for plain-TCP
// self-host Valkey/Redis.
// "tls": false
},
// ── Email (transactional) ───────────────────────────────────────────
// Nodemailer transport — used for password resets, email confirmation, etc.
"email": {
"from": "\"Puter\" <[email protected]>",
"host": "smtp.example.com",
"port": 587,
"secure": false,
"service": "",
"auth": {
"user": "",
"pass": ""
}
},
// ── Rate limiting ───────────────────────────────────────────────────
// `memory` for single-node, `redis` for multi-node (default), `kv` for
// dynamo-backed counters.
"rate_limit": {
"backend": "redis"
},
// ── AI / integration providers ──────────────────────────────────────
// All AI drivers (chat, image, video, TTS, OCR, STT) read from here.
// Provider id == driver-side identifier. Leave empty / omit to disable.
"providers": {
// ─ Chat / completion ─
"claude": { "apiKey": "" },
"openai-completion": { "apiKey": "" },
"azure-openai": {
"apiKey": "",
"apiURL": ""
},
"gemini": { "apiKey": "" },
"groq": { "apiKey": "" },
"deepseek": { "apiKey": "" },
"mistral": { "apiKey": "" },
"xai": { "apiKey": "" },
"moonshot": { "apiKey": "" },
"minimax": {
"apiKey": "",
"apiBaseUrl": "https://api.minimax.io/v1"
},
"openrouter": {
"apiKey": "",
"apiBaseUrl": "https://openrouter.ai/api/v1"
},
"together-ai": { "apiKey": "" },
// Local Ollama. `enabled: false` skips the auto-probe at startup
// (otherwise Puter logs ECONNREFUSED on every boot when no Ollama
// is running).
"ollama": {
"enabled": false,
"apiBaseUrl": "http://localhost:11434"
},
// ─ Image generation ─
"openai-image-generation": { "apiKey": "" },
"gemini-image-generation": { "apiKey": "" },
"together-image-generation": { "apiKey": "" },
"cloudflare-image-generation": {
"apiToken": "",
"accountId": "",
"apiBaseUrl": "https://api.cloudflare.com/client/v4"
},
"xai-image-generation": { "apiKey": "" },
// ─ Video generation ─
"openai-video-generation": { "apiKey": "" },
"together-video-generation": { "apiKey": "" },
"gemini-video-generation": { "apiKey": "" },
// ─ Speech / OCR ─
"openai": { "apiKey": "" },
"elevenlabs": {
"apiKey": "",
"apiBaseUrl": "https://api.elevenlabs.io",
"defaultVoiceId": "",
"speechToSpeechModelId": ""
},
"aws-polly": {
"access_key": "",
"secret_key": "",
"region": "us-west-2"
},
"aws-textract": {
"access_key": "",
"secret_key": "",
"region": "us-west-2"
},
"mistral-ocr": { "apiKey": "" }
},
// ── GUI / static mounts ─────────────────────────────────────────────
"gui_assets_root": "./src/gui",
"gui_profile": "development",
"builtin_apps": {
"dev-center": "./src/dev-center"
},
// Force the bundled GUI even in dev — set true when running from a
// pre-built tree without webpack-dev-server.
"use_bundled_gui": false,
"gui_bundle": "/dist/bundle.min.js",
"gui_css": "/dist/bundle.min.css",
"gui_puterjs_bundle": "https://js.puter.com/v2/",
"gui_params": {
"title": "Puter",
"short_description": "Your personal cloud computer",
"social_media_image": ""
},
// Optional roots for native app bundles and custom puter.js builds.
"native_apps_root": "",
"client_libs_root": "",
"puterjs_root": "./src/puter-js/dist",
// ── Feature flags (whoami extension) ────────────────────────────────
// Flat `{ flag_name: boolean }` bag. Server-only by default — flags are
// only surfaced to the client if their key is on the allowlist in
// `extensions/whoami.ts` (CLIENT_VISIBLE_FEATURE_FLAGS).
"feature_flags": {
"example_flag": false
},
// ── Misc / safety ───────────────────────────────────────────────────
// TLDs / domains rejected at signup (prod only).
"blockedEmailDomains": [],
"support_email": "[email protected]",
// Worker / subdomain names users can't claim.
"reserved_words": [],
"max_subdomains_per_user": 10,
"server_health": {
"db_liveness_latency_fail_ms": 1500,
"stale_health_loop_fail_ms": 0
},
// ── Extensions ──────────────────────────────────────────────────────
// Directories scanned for extension entrypoints (`*.ts` / subdirs).
"extensions": [
"./extensions"
],
// ── Metering ────────────────────────────────────────────────────────
// When true, all metering checks pass — no per-actor limits enforced.
"unlimitedMetering": false
}
-38
View File
@@ -1,38 +0,0 @@
# Documentation for Robots
Hello, if you're an AI agent then you're reading the correct documentation.
Here are a few important notes:
- Puter is probably already cloned and configured, so avoid any setup
or configuration steps unless explicitly asked to perform them.
- Anything under `/src` (relative to the root of the repo) is probably
a workspace module. That means different directories might have different
code styles or use different import mechanisms (ESM vs CJS). Try to keep
changes consistent in the scope of where they are.
# Backend
Any file under `src/backend` that extends **BaseService** is called a
"backend service". Backend services can implement "traits". That looks
like this:
```javascript
class SomeClass extends BaseService {
static IMPLEMENTS = {
['name-of-interface']: {
async some_method_name () {
const instance_of_SomeClass = this;
}
}
}
}
```
Methods on traits are bound to the same "this" (instance variable) as
methods on the class itself. Trait methods cannot be indexed from the
instance variable; instead common functionality is usually moved to
regular instance methods which typically have an underscore at the end
of their name.
# Furher Documentation
Proceed to read the README.md document beside this file.
-214
View File
@@ -1,214 +0,0 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" version="25.0.1">
<diagram name="Page-1" id="5LlmtnmR4draSSsTtYyD">
<mxGraphModel grid="1" page="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" pageScale="1" pageWidth="1100" pageHeight="850" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="skmebJAFKBwesmhEX21R-64" value="Legacy Notice" style="rounded=1;whiteSpace=wrap;html=1;align=left;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="540" y="555" width="130" height="20" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-8" value="only most important directories shown" style="rounded=1;whiteSpace=wrap;html=1;align=right;" vertex="1" parent="1">
<mxGeometry x="380" y="50" width="230" height="20" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-1" value="src/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="360" y="110" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-2" value="Puter Repository" style="rounded=0;whiteSpace=wrap;html=1;fontSize=18;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="240" y="40" width="160" height="40" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-4" value="Every directory under src/ is a node module." style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;size=14;align=left;" vertex="1" parent="1">
<mxGeometry x="470" y="110" width="260" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-5" value="" style="endArrow=classic;html=1;rounded=0;edgeStyle=elbowEdgeStyle;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-2" target="skmebJAFKBwesmhEX21R-1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="490" y="260" as="sourcePoint" />
<mxPoint x="540" y="210" as="targetPoint" />
<Array as="points">
<mxPoint x="320" y="125" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-6" value="backend/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="440" y="190" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-7" value="gui/src/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="440" y="1000" width="80" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-10" value="" style="endArrow=none;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-1" target="skmebJAFKBwesmhEX21R-4">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="490" y="260" as="sourcePoint" />
<mxPoint x="540" y="210" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-12" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-1" target="skmebJAFKBwesmhEX21R-6">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="490" y="260" as="sourcePoint" />
<mxPoint x="540" y="210" as="targetPoint" />
<Array as="points">
<mxPoint x="400" y="205" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-14" value="extensions/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="520" y="1080" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-15" value="Puter GUI Extensions" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="730" y="1080" width="180" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-16" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-14" target="skmebJAFKBwesmhEX21R-15">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="530" y="980" as="sourcePoint" />
<mxPoint x="580" y="930" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-15" target="skmebJAFKBwesmhEX21R-15">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-18" value="UI/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="520" y="1120" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-19" value="Puter Desktop" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="730" y="1120" width="180" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-20" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-18" target="skmebJAFKBwesmhEX21R-19">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="670" y="1098" as="sourcePoint" />
<mxPoint x="740" y="1098" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-21" value="services/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="520" y="1160" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-22" value="Frontend Services" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="730" y="1160" width="180" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-23" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-21" target="skmebJAFKBwesmhEX21R-22">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="670" y="1138" as="sourcePoint" />
<mxPoint x="740" y="1138" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-24" value="initgui.js" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="520" y="1200" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-25" value="Launches services and initializes the GUI" style="rounded=1;whiteSpace=wrap;html=1;fontSize=13;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="730" y="1200" width="180" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-26" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-24" target="skmebJAFKBwesmhEX21R-25">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="670" y="1185" as="sourcePoint" />
<mxPoint x="740" y="1185" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-27" value="IPC.js" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="520" y="1240" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-28" value="Manages communication between Desktop and apps" style="rounded=1;whiteSpace=wrap;html=1;fontSize=13;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="730" y="1240" width="180" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-29" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-27" target="skmebJAFKBwesmhEX21R-28">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="670" y="1225" as="sourcePoint" />
<mxPoint x="740" y="1225" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-46" value="modules/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="520" y="280" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-47" value="New Code goes Here" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="730" y="280" width="180" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-48" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-46" target="skmebJAFKBwesmhEX21R-47">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="530" y="180" as="sourcePoint" />
<mxPoint x="580" y="130" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-49" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-47" target="skmebJAFKBwesmhEX21R-47">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-50" value="services/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="520" y="320" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-51" value="Core Module services" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="730" y="320" width="180" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-52" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-50" target="skmebJAFKBwesmhEX21R-51">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="670" y="298" as="sourcePoint" />
<mxPoint x="740" y="298" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-53" value="routers/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="520" y="480" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-54" value="HTTP endpoints" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
<mxGeometry x="730" y="480" width="180" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-55" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-53" target="skmebJAFKBwesmhEX21R-54">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="670" y="458" as="sourcePoint" />
<mxPoint x="740" y="458" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-62" value="&lt;div&gt;&lt;span style=&quot;background-color: initial;&quot;&gt;This is the original directory where routes&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: initial;&quot;&gt;were placed. New services should use the&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: initial;&quot;&gt;&quot;Endpoint&quot; class to define routes.&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: initial;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: initial;&quot;&gt;Examples:&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: initial;&quot;&gt;- PermissionAPIService.js&lt;/span&gt;&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;background-color: initial;&quot;&gt;- NotificationService.js&lt;/span&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;- KernelInfoService.js&lt;/div&gt;&lt;div&gt;&lt;span style=&quot;color: rgba(0, 0, 0, 0); font-family: monospace; font-size: 0px; text-wrap: nowrap;&quot;&gt;%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22extensions%2F%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfontSize%3D16%3BfillColor%3D%23e1d5e7%3BstrokeColor%3D%239673a6%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22520%22%20y%3D%22400%22%20width%3D%22140%22%20height%3D%2230%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%223%22%20value%3D%22Puter%20GUI%20Extensions%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfontSize%3D16%3BfillColor%3D%23d5e8d4%3BstrokeColor%3D%2382b366%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22730%22%20y%3D%22400%22%20width%3D%22180%22%20height%3D%2230%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%224%22%20value%3D%22%22%20style%3D%22endArrow%3Dclassic%3Bhtml%3D1%3Brounded%3D0%3B%22%20edge%3D%221%22%20source%3D%222%22%20target%3D%223%22%20parent%3D%221%22%3E%3CmxGeometry%20width%3D%2250%22%20height%3D%2250%22%20relative%3D%221%22%20as%3D%22geometry%22%3E%3CmxPoint%20x%3D%22530%22%20y%3D%22300%22%20as%3D%22sourcePoint%22%2F%3E%3CmxPoint%20x%3D%22580%22%20y%3D%22250%22%20as%3D%22targetPoint%22%2F%3E%3C%2FmxGeometry%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%225%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BexitX%3D0.5%3BexitY%3D1%3BexitDx%3D0%3BexitDy%3D0%3B%22%20edge%3D%221%22%20source%3D%223%22%20target%3D%223%22%20parent%3D%221%22%3E%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%226%22%20value%3D%22UI%2F%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfontSize%3D16%3BfillColor%3D%23e1d5e7%3BstrokeColor%3D%239673a6%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22520%22%20y%3D%22440%22%20width%3D%22140%22%20height%3D%2230%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%227%22%20value%3D%22Puter%20Desktop%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfontSize%3D16%3BfillColor%3D%23d5e8d4%3BstrokeColor%3D%2382b366%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22730%22%20y%3D%22440%22%20width%3D%22180%22%20height%3D%2230%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%228%22%20value%3D%22%22%20style%3D%22endArrow%3Dclassic%3Bhtml%3D1%3Brounded%3D0%3B%22%20edge%3D%221%22%20source%3D%226%22%20target%3D%227%22%20parent%3D%221%22%3E%3CmxGeometry%20width%3D%2250%22%20height%3D%2250%22%20relative%3D%221%22%20as%3D%22geometry%22%3E%3CmxPoint%20x%3D%22670%22%20y%3D%22418%22%20as%3D%22sourcePoint%22%2F%3E%3CmxPoint%20x%3D%22740%22%20y%3D%22418%22%20as%3D%22targetPoint%22%2F%3E%3C%2FmxGeometry%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%229%22%20value%3D%22services%2F%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfontSize%3D16%3BfillColor%3D%23e1d5e7%3BstrokeColor%3D%239673a6%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22520%22%20y%3D%22480%22%20width%3D%22140%22%20height%3D%2230%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%2210%22%20value%3D%22Frontend%20Services%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfontSize%3D16%3BfillColor%3D%23d5e8d4%3BstrokeColor%3D%2382b366%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22730%22%20y%3D%22480%22%20width%3D%22180%22%20height%3D%2230%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%2211%22%20value%3D%22%22%20style%3D%22endArrow%3Dclassic%3Bhtml%3D1%3Brounded%3D0%3B%22%20edge%3D%221%22%20source%3D%229%22%20target%3D%2210%22%20parent%3D%221%22%3E%3CmxGeometry%20width%3D%2250%22%20height%3D%2250%22%20relative%3D%221%22%20as%3D%22geometry%22%3E%3CmxPoint%20x%3D%22670%22%20y%3D%22458%22%20as%3D%22sourcePoint%22%2F%3E%3CmxPoint%20x%3D%22740%22%20y%3D%22458%22%20as%3D%22targetPoint%22%2F%3E%3C%2FmLine truncated
<mxGeometry x="645" y="545" width="270" height="135" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-63" value="" style="endArrow=classic;html=1;rounded=0;edgeStyle=elbowEdgeStyle;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-62" target="skmebJAFKBwesmhEX21R-54">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="730" y="560" as="sourcePoint" />
<mxPoint x="780" y="510" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-65" value="structured/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="520" y="700" width="140" height="20" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-66" value="unstructured/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="520" y="720" width="140" height="20" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-68" value="&lt;div&gt;These directories mostly contain code for&lt;/div&gt;&lt;div&gt;the permission system to make it easier to maintain.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;structured/&amp;lt;pattern&amp;gt;/ contains code that is formatted according to &quot;&amp;lt;pattern&amp;gt;&quot;&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;unstructured/ contains miscellaneous code used by the permission system.&lt;/div&gt;" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;size=18;fillColor=#f5f5f5;strokeColor=#666666;align=left;verticalAlign=top;fontSize=13;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="730" y="700" width="270" height="150" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-69" value="" style="endArrow=classic;html=1;rounded=0;edgeStyle=elbowEdgeStyle;elbow=vertical;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-68" target="skmebJAFKBwesmhEX21R-65">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="730" y="680" as="sourcePoint" />
<mxPoint x="780" y="630" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-70" value="" style="endArrow=classic;html=1;rounded=0;edgeStyle=elbowEdgeStyle;elbow=vertical;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-68" target="skmebJAFKBwesmhEX21R-66">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="740" y="720" as="sourcePoint" />
<mxPoint x="670" y="720" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-71" value="these should go to:&lt;div&gt;modules/auth/&lt;/div&gt;&lt;div&gt;in the future&lt;/div&gt;" style="rounded=1;whiteSpace=wrap;html=1;glass=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="520" y="760" width="140" height="50" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-72" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-71" target="skmebJAFKBwesmhEX21R-66">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="730" y="680" as="sourcePoint" />
<mxPoint x="780" y="630" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-73" value="util/" style="rounded=0;whiteSpace=wrap;html=1;fontSize=16;fillColor=#e1d5e7;strokeColor=#9673a6;" vertex="1" parent="1">
<mxGeometry x="520" y="880" width="140" height="30" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-74" value="&lt;div&gt;Utilities used by various services in Puter&#39;s backend.&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;These should be incrementally moved to individual `lib` directories in modules/&lt;/div&gt;" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;size=18;fillColor=#f5f5f5;strokeColor=#666666;align=left;verticalAlign=top;fontSize=13;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="730" y="880" width="270" height="90" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-75" value="" style="endArrow=classic;html=1;rounded=0;edgeStyle=elbowEdgeStyle;elbow=vertical;" edge="1" parent="1" source="skmebJAFKBwesmhEX21R-74" target="skmebJAFKBwesmhEX21R-73">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="740" y="740" as="sourcePoint" />
<mxPoint x="670" y="740" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-76" value="Migration Notice" style="rounded=1;whiteSpace=wrap;html=1;align=left;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=14;" vertex="1" parent="1">
<mxGeometry x="540" y="400" width="130" height="20" as="geometry" />
</mxCell>
<mxCell id="skmebJAFKBwesmhEX21R-77" value="&lt;div&gt;Services here should be incrementally&lt;/div&gt;&lt;div&gt;migrated into the modules/ directory.&lt;/div&gt;" style="shape=note;whiteSpace=wrap;html=1;backgroundOutline=1;darkOpacity=0.05;size=18;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="650" y="390" width="270" height="50" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

-45
View File
@@ -1,45 +0,0 @@
# Puter Documentation
Hi, you've found Puter's wiki page on GitHub! If you were looking for
something else, you might find it in the links below.
All of the wiki docs are generated from `doc/` directories in the main
repository, so it's best to edit docs there rather than here.
## Users
If you have general questions about using [Puter](https://puter.com),
our [community Discord](https://discord.gg/PQcx7Teh8u) and
[subreddit](https://www.reddit.com/r/puter/) are good places
to ask questions.
## Deployers
- [Hosting Instructions](./self-hosters/instructions.md)
- [Configuration](./self-hosters/config.md)
- [Domain Setup](./self-hosters/domains.md)
- [Support Levels](./self-hosters/support.md)
## App Developer Links
- [developer.puter.com](https://developer.puter.com)
- [docs.puter.com](https://docs.puter.com)
- share your apps on [Reddit](https://www.reddit.com/r/puter/) or
[Discord](https://discord.gg/PQcx7Teh8u)
## Contributor Documentation
### Where to Start
Start with [Repo Structure and Tooling](./contributors/structure.md).
### Index
- **Conventions**
- [Repo Structure and Tooling](./contributors/structure.md)
- How directories and files are organized in our GitHub repo
- What tools are used to build parts of Puter
- [Comment Prefixes](./contributors/comment_prefixes.md)
- A convention we use for line comments in code
- [Frontend Documentation](/src/gui/doc)
- [Backend Documentation](/src/backend/doc)
- [Extensions](./contributors/extensions/)
-5
View File
@@ -1,5 +0,0 @@
# API Documentation
Note that this documentation is different from the [puter.js docs](https://docs.puter.com).
The scope of the documentation in this directory includes both stable API endpoints that
are used by **puter.js**, as well as API endpoints that may be subject to future changes.
-60
View File
@@ -1,60 +0,0 @@
## Puter Drivers
### **POST** `/drivers/call`
#### Notes
- **HTTP response status** -
A successful driver response, even if the response is an error message, will always have HTTP status `200`. Note that sometimes this will include rate limit and usage limit errors as well.
This endpoint allows you to call a Puter driver. Whether or not the
driver call fails, this endpoint will respond with HTTP 200 OK.
When a driver call fails, you will get a JSON response from the driver
with
#### Parameters
Parameters are provided in the request body. The content type of the
request should be `application/json`.
- **interface:** `string`
- **description:** The type of driver to call. For example,
LLMs use the interface called `puter-chat-completion`.
- **service:** `string`
- **description:** The name of the service to use. For example, the `claude` service might be used for `puter-chat-completion`.
- **method:** `string`
- **description:** The name of the method to call. For example, LLMs implement `complete` which does a chat completion, and `list` which lists models.
- **args:** `object`
- **description:** Parametized arguments for the driver call. For example, `puter-chat-completion`'s `complete` method supports the arguments `messages` and `temperature` (and others), so you might set this to `{ "messages": [...], "temperature": 1.2 }`
#### Example
```json
{
"interface": "<name of interface>",
"service": "<name of service>",
"method": "<name of method>",
"args": { "parametized": "arguments" }
}
```
#### Response
- **Error Response** - Driver error responses will always have **status 200**, content type `application/json`, and a response body in this format:
```json
{
"success": false,
"error": {
"code": "string identifier for the error",
"message": "some message about the error",
}
}
```
- **Success Response** - The success response is either a JSON response
wrapped in `{ "success": true, "result": ___ }`, or a response with a
`Content-Type` that is **not** `application/json`.
```json
{
"success": true,
"result": {}
}
```
-219
View File
@@ -1,219 +0,0 @@
# Group Endpoints
## POST `/group/create` (auth required)
### Description
Creates a group and returns a UID (UUID formatted).
Groups do not have names, or any other descriptive attributes.
Instead they are always identified with a UUID, and they have
a `metadata` property.
The `metadata` property will always be given back to the client
in the same way it was provided. The `extra` property, also an
object, may be changed by the backend. The behavior of setting
any property on `extra` is currently undefined as all properties
are reserved for future use.
### Parameters
- **metadata:** _- optional_
- **accepts:** `object`
- **description:** arbitrary metadata to describe the group
- **extra:** _- optional_
- **accepts:** `object`
- **description:** extra parameters (server may change these)
### Request Example
```javascript
await fetch(`${window.api_origin}/group/create`, {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
metadata: { title: 'Some Title' }
}),
"method": "POST",
});
// { uid: '9c644a1c-3e43-4df4-ab67-de5b68b235b6' }
```
### Response Example
```json
{
"uid": "9c644a1c-3e43-4df4-ab67-de5b68b235b6"
}
```
## POST `/group/add-users`
### Description
Adds one or more users to a group
### Parameters
- **uid:** _- required_
- **accepts:** `string`
UUID of an existing group
- **users:** `Array<string>`
usernames of users to add to the group
### Request Example
```javascript
await fetch(`${window.api_origin}/group/add-users`, {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
uid: '9c644a1c-3e43-4df4-ab67-de5b68b235b6',
users: ['first_user', 'second_user'],
}),
"method": "POST",
});
```
## POST `/group/remove-users`
### Description
Remove one or more users from a group
### Parameters
- **uid:** _- required_
- **accepts:** `string`
UUID of an existing group
- **users:** `Array<string>`
usernames of users to remove from the group
### Request Example
```javascript
await fetch(`${window.api_origin}/group/add-users`, {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
uid: '9c644a1c-3e43-4df4-ab67-de5b68b235b6',
users: ['first_user', 'second_user'],
}),
"method": "POST",
});
```
## GET `/group/list`
### Description
List groups associated with the current user
### Parameters
_none_
### Response Example
```json
{
"owned_groups": [
{
"uid": "c3bd4047-fc65-4da8-9363-e52195890de4",
"metadata": {},
"members": [
"default_user"
]
}
],
"in_groups": [
{
"uid": "c3bd4047-fc65-4da8-9363-e52195890de4",
"metadata": {},
"members": [
"default_user"
]
}
]
}
```
# Group Permission Endpoints
## POST `/grant-user-group`
Grant permission from the current user to a group.
This creates an association between the user and the
group for this permission; the group will only have
the permission effectively while the user who granted
permission has the permission.
### Parameters
- **group_uid:** _- required_
- **accepts:** `string`
UUID of an existing group
- **permission:** _- required_
- **accepts:** `string`
A permission string
### Request Example
```javascript
await fetch("http://puter.localhost:4100/auth/grant-user-group", {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
group_uid: '9c644a1c-3e43-4df4-ab67-de5b68b235b6',
permission: 'fs:/someuser/somedir/somefile:read'
}),
"method": "POST",
});
```
## POST `/revoke-user-group`
Revoke permission granted from the current user
to a group.
### Parameters
- **group_uid:** _- required_
- **accepts:** `string`
UUID of an existing group
- **permission:** _- required_
- **accepts:** `string`
A permission string
### Request Example
```javascript
await fetch("http://puter.localhost:4100/auth/grant-user-group", {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
group_uid: '9c644a1c-3e43-4df4-ab67-de5b68b235b6',
permission: 'fs:/someuser/somedir/somefile:read'
}),
"method": "POST",
});
```
- > **TODO** figure out how to manage documentation that could
reasonably show up in two files. For example: this is a group
endpoint as well as a permission system endpoint.
(architecturally it's a permission system endpoint, and
the permissions feature depends on the groups feature;
at least until a time when PermissionService is refactored
so a service like GroupService can mutate the permission
check sequences)
-112
View File
@@ -1,112 +0,0 @@
# Notification Endpoints
Endpoints for managing notifications.
## POST `/notif/mark-ack` (auth required)
### Description
The `/notif/mark-ack` endpoint marks the specified notification
as "acknowledged". This indicates that the user has chosen to either
dismiss or act on this notification.
### Parameters
| Name | Description | Default Value |
| ---- | ----------- | -------- |
| uid | UUID associated with the notification | **required** |
### Response
This endpoint responds with an empty object (`{}`).
## POST `/notif/mark-read` (auth required)
### Description
The `/notif/mark-read` endpoint marks that the specified notification
has been shown to the user. It will not "pop up" as a new notification
if they load the gui again.
### Parameters
| Name | Description | Default Value |
| ---- | ----------- | -------- |
| uid | UUID associated with the notification | **required** |
### Response
This endpoint responds with an empty object (`{}`).
### Request Example
```javascript
await fetch("https://api.puter.local/notif/mark-read", {
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
body: JSON.stringify({
uid: 'a14ea3d5-828b-42f9-9613-35f43b0a3cb8',
}),
method: "POST",
});
```
## ENTITY STORAGE `puter-notifications`
The `puter-notifications` driver is an Entity Storage driver.
It is read-only.
### Request Examples
#### Select Unread Notifications
```javascript
await fetch("http://api.puter.localhost:4100/drivers/call", {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
interface: 'puter-notifications',
method: 'select',
args: { predicate: ['unread'] }
}),
"method": "POST",
});
```
#### Select First 200 Notifications
```javascript
await fetch("http://api.puter.localhost:4100/drivers/call", {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
interface: 'puter-notifications',
method: 'select',
args: {}
}),
"method": "POST",
});
```
#### Select Next 200 Notifications
```javascript
await fetch("http://api.puter.localhost:4100/drivers/call", {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
interface: 'puter-notifications',
method: 'select',
args: { offset: 200 }
}),
"method": "POST",
});
```
-367
View File
@@ -1,367 +0,0 @@
# Share Endpoints
Share endpoints allow sharing files with other users.
## POST `/share` (auth required)
### Description
The `/share` endpoint shares 1 or more filesystem items
with one or more recipients. The recipients will receive
some notification about the shared item, making this
different from calling `/grant-user-user` with a permission.
When users are **specified by email** they will receive
a [share link](./concepts/share-link.md).
Each item specified in the `shares` property is a tag-typed
object of type `fs-share` or `app-share`.
#### File Shares (`fs-share`)
File shares grant permission to a file or directory. By default
this is read permission. If `access` is specified as `"write"`,
then write permission will be granted.
#### App Shares (`app-share`)
App shares grant permission to read a protected app.
##### subdomain permission
If there is a subdomain associated with the app, and the owner
of the subdomain is the same as the owner of the app, then
permission to access the subdomain will be granted.
Note that the subdomain is only associated if the subdomain
entry has `associated_app_id` set according to the app's id,
and will not be considered "associated" if only the index_url
happens to match the subdomain url.
##### appdata permission
If the app has `shared_appdata` set to `true` in its metadata
object, the recipient of the share will also get write permission
to the app owner's corresponding appdata directory. The appdata
directory must exist for this to work as expected
(otherwise the permission rewrite rule fails since the uuid
can't be determined).
### Example
```json
{
"recipients": [
"user_that_gets_shared_to",
"[email protected]"
],
"shares": [
{
"$": "app-share",
"name": "some-app-name"
},
{
"$": "app-share",
"uid": "app-SOME-APP-UID"
},
{
"$": "fs-share",
"path": "/some/file/or/directory"
},
{
"$": "fs-share",
"path": "SOME-FILE-UUID"
}
]
}
```
### Parameters
- **recipients** _- required_
- **accepts:** `string | Array<string>`
- **description:**
recipients for the filesystem entries being shared.
- **notes:**
- validation on `string`: email or username
- requirement of at least one value
- **shares:** _- required_
- **accepts:** `object | Array<object>`
- object is [type-tagged](./type-tagged.md)
- type is either [file-share](./types/file-share.md)
or [app-share](./types/app-share.md)
- **notes:**
- requirement that file/directory or app exists
- requirement of at least one entry
- **dry_run:** _- optional_
- **accepts:** `bool`
- **description:**
when true, only validation will occur
### Response
- **$:** `api:share`
- **$version:** `v0.0.0`
- **status:** one of: `"success"`, `"mixed"`, `"aborted"`
- **recipients:** array of: `api:status-report` or
`heyputer:api/APIError`
- **paths:** array of: `api:status-report` or
`heyputer:api/APIError`
- **dry_run:** `true` if present
### Request Example
```javascript
await fetch("http://puter.localhost:4100/share", {
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
body: JSON.stringify({
recipients: [
"user_that_gets_shared_to",
"[email protected]"
],
shares: [
{
$: "app-share",
name: "some-app-name"
},
{
$: "app-share",
uid: "app-SOME-APP-UID"
},
{
$: "fs-share",
path: "/some/file/or/directory"
},
{
$: "fs-share",
path: "SOME-FILE-UUID"
}
]
}),
method: "POST",
});
```
### Success Response
```json
{
"$": "api:share",
"$version": "v0.0.0",
"status": "success",
"recipients": [
{
"$": "api:status-report",
"status": "success"
}
],
"paths": [
{
"$": "api:status-report",
"status": "success"
}
],
"dry_run": true
}
```
### Error response (missing file)
```json
{
"$": "api:share",
"$version": "v0.0.0",
"status": "mixed",
"recipients": [
{
"$": "api:status-report",
"status": "success"
}
],
"paths": [
{
"$": "heyputer:api/APIError",
"code": "subject_does_not_exist",
"message": "File or directory not found.",
"status": 404
}
],
"dry_run": true
}
```
### Error response (missing user)
```json
{
"$": "api:share",
"$version": "v0.0.0",
"status": "mixed",
"recipients": [
{
"$": "heyputer:api/APIError",
"code": "user_does_not_exist",
"message": "The user `non_existing_user` does not exist.",
"username": "non_existing_user",
"status": 422
}
],
"paths": [
{
"$": "api:status-report",
"status": "success"
}
],
"dry_run": true
}
```
## POST `/sharelink/check` (no auth)
### Description
The `/sharelink/check` endpoint verifies that a token provided
by a share link is valid.
### Example
```javascript
await fetch(`${config.api_origin}/sharelink/check`, {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
token: '...',
}),
"method": "POST",
});
```
### Parameters
- **token:** _- required_
- **accepts:** `string`
The token from the querystring parameter
### Response
A type-tagged object, either of type `api:share` or `api:error`
### Success Response
```json
{
"$": "api:share",
"uid": "836671d4-ac5d-4bd3-bc0a-ec357e0d8f02",
"email": "[email protected]"
}
```
### Error Response
```json
{
"$": "api:error",
"message":"Field `token` is required.",
"key":"token",
"code":"field_missing"
}
```
## POST `/sharelink/apply` (no auth)
### Description
The `/sharelink/apply` endpoint applies a share to the current
user **if and only if** that user's email is confirmed and matches
the email associated with the share.
### Example
```javascript
await fetch(`${config.api_origin}/sharelink/apply`, {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
uid: '836671d4-ac5d-4bd3-bc0a-ec357e0d8f02',
}),
"method": "POST",
});
```
### Parameters
- **uid:** _- required_
- **accepts:** `string`
The uid of an existing share, received using `/sharelink/check`
### Response
A type-tagged object, either of type `api:status-report` or `api:error`
### Success Response
```json
{"$":"api:status-report","status":"success"}
```
### Error Response
```json
{
"message": "This share can not be applied to this user.",
"code": "can_not_apply_to_this_user"
}
```
## POST `/sharelink/request` (no auth)
### Description
The `/sharelink/request` endpoint requests the permissions associated
with a share link to the issuer of the share (user that sent the share).
This can be used when a user is logged in, but that user's email does
not match the email associated with the share.
### Example
```javascript
await fetch(`${config.api_origin}/sharelink/request`, {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
uid: '836671d4-ac5d-4bd3-bc0a-ec357e0d8f02',
}),
"method": "POST",
});
```
### Parameters
- **uid:** _- required_
- **accepts:** `string`
The uid of an existing share, received using `/sharelink/check`
### Response
A type-tagged object, either of type `api:status-report` or `api:error`
### Success Response
```json
{"$":"api:status-report","status":"success"}
```
### Error Response
```json
{
"message": "This share is already valid for this user; POST to /apply for access",
"code": "no_need_to_request"
}
```
-79
View File
@@ -1,79 +0,0 @@
# Type-Tagged Objects
```js
{
"$": "some-type",
"$version": "0.0.0",
"some_property": "some value",
}
```
## What's a Type-Tagged Object?
Type-Tagged objects are a convention understood by Puter's backend
to communicate meta information along with a JSON object.
The key feature of Type-Tagged Objects is the type key: `"$"`.
## Why Type-Tagged Objects?
The primary reason: to have a consistent convention we can use
anywhere.
- Since other services rarely use `$` in their property names,
we can safely use this without introducing reserved words and
re-mapping property names.
- Some places we use this convention might not need it, but
staying consistent means API end-users can
[do more with less code](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).
## Specification
- The `"$"` key indicates a type (or class) of object
- Any other key beginning with `$` is a **meta-key**
- Other keys are not allowed to contain `$`
- `"$version"` must follow [semver](https://semver.org/)
- Keys with multiple `"$"` symbols are reserved for future use
## Alternative Representations
Puter's API will always send results in the format described
above, which is called the "Standard Representation"
Any endpoint which accepts a Type-Tagged Object will also
accept these alternative representations:
### Structured Representation
Depending on the architecture of your client, this format
may be more convenient to work with:
```json
{
"$": "$meta-body",
"type": "some-type",
"meta": { "version": "0.0.0" },
"body": { "some_property": "some value" }
}
```
### Array Representation
In the array representation, meta values go at the end.
```json
["some-type",
{ "some_property": "some value" },
{ "version": "0.0.0" }
]
```
If the second element of the list is not an object, it
will implicitly be placed in a property called value.
The following are equivalent:
```json
["some-type", "hello"]
```
```json
["some-type", { "value": "hello" }]
```
+104
View File
@@ -0,0 +1,104 @@
# Backend Architecture
Loosely inspired by the Controller–Service–Repository pattern with dependency injection. The backend is organized as a stack of layers where each layer only depends on the layers beneath it, and `PuterServer` ([src/backend/server.ts](../src/backend/server.ts)) instantiates each layer in order and hands the instances down to the next.
## Layers
```mermaid
block-beta
columns 1
REQ["HTTP request"]
CTRL["Controllers — route handlers, gates, I/O shaping"]
DRV["Drivers (optional) — RPC handlers on /drivers/*"]
SVC["Services — business logic, no auth"]
STR["Stores — persistence / domain shapes"]
CLI["Clients — sql, redis, s3, dynamo, email, …"]
CFG["Config — IConfig"]
REQ --> CTRL
CTRL --> DRV
DRV --> SVC
SVC --> STR
STR --> CLI
CLI --> CFG
style REQ fill:#0ea5e9,stroke:#0369a1,color:#fff
style CTRL fill:#1d4ed8,stroke:#1e3a8a,color:#fff
style DRV fill:#2563eb,stroke:#1e40af,color:#fff
style SVC fill:#4f46e5,stroke:#3730a3,color:#fff
style STR fill:#7c3aed,stroke:#5b21b6,color:#fff
style CLI fill:#9333ea,stroke:#6b21a8,color:#fff
style CFG fill:#334155,stroke:#1e293b,color:#fff
```
Each layer only depends on the layers beneath it, and every dependency is injected through the constructor by `PuterServer`. Extensions sit alongside this stack and can register into any layer — see [Extensions](#extensions) below.
| Layer | Lives in | Responsibility |
| --- | --- | --- |
| **Controllers** | [src/backend/controllers/](../src/backend/controllers/) | Route handlers. Parse + validate input, apply per-route gates (auth, subdomain, rate limit, body parsers — see `RouteOptions`), call into services, format responses. |
| **Drivers** | [src/backend/drivers/](../src/backend/drivers/) | Optional. RPC-style handlers exposed over the `/drivers/*` surface (`puter-kvstore`, `puter-chat-completion`, …). A driver is a thin shell that validates RPC inputs and calls into services/stores; controllers can hold a typed reference to drivers when they need the same logic over HTTP. |
| **Services** | [src/backend/services/](../src/backend/services/) | Business logic. Assume the caller is already authenticated/authorized — services do not run auth gates themselves. |
| **Stores** | [src/backend/stores/](../src/backend/stores/) | Persistence and storage logic. Wraps clients with the domain shape services consume (rows, entities, KV namespaces). |
| **Clients** | [src/backend/clients/](../src/backend/clients/) | Adapters for external/internal services (sql, redis, s3, dynamodb, email, event bus, …). Knows protocols, not domain concepts. |
| **Config** | `config.*.json` → `IConfig` | The flat, typed config object every layer receives at construction. |
Each layer receives the layers beneath it through its constructor, so dependencies are explicit and traceable from `PuterServer`. A controller does not reach into a client directly; if it needs one, the right move is usually a service.
## Entry point: `PuterServer`
`PuterServer` is the bootstrap. It:
1. Loads any configured extension directories (`config.extensions`) so extensions can register before instantiation begins.
2. Instantiates each layer in order — clients → stores → services → drivers → controllers — merging in anything extensions have registered for that layer.
3. Wires global middleware, mounts controller routes through `PuterRouter` (which translates `RouteOptions` into the gate/parser middleware chain), and mounts extension routes through the same materializer.
4. Fires `onServerStart` hooks across every layer once HTTP is listening, and `onServerPrepareShutdown` / `onServerShutdown` on the way down.
## Context (ALS)
We use [`Context`](../src/backend/core/context.ts) — backed by `AsyncLocalStorage` — to carry per-request state without threading it through every function signature. It is used **sparingly**, mostly for `actor` and `req`. The request-context middleware opens a scope per request after the auth probe runs; anything inside a request handler can call `Context.get('actor')` / `Context.get('req')` instead of plumbing it as an argument.
Prefer explicit arguments. Reach for `Context` only when the value is truly request-scoped and would otherwise need to thread through many layers.
## Extensions
Extensions live alongside core ([packages/puter/extensions/](../extensions/)) and parallel the layered stack. They are meant for **non-crucial parts of the system** — things Puter still works without if removed.
- **Good extensions**: [thumbnails](../extensions/thumbnails.ts), [serverInfo](../extensions/serverInfo.ts), [devWatcher](../extensions/devWatcher.ts) — opt-in features cleanly bolted on.
- **Should probably be core**: [metering](../extensions/metering.ts), [appTelemetry](../extensions/appTelemetry.ts) — clients now expect these to be present, so the "extension" framing is misleading.
- **Shouldn't have been an extension**: [whoami](../extensions/whoami.ts) — it's load-bearing for every authenticated client. Keep this one in mind as a cautionary example when deciding whether something belongs in an extension.
### Extension API
The `extension` global ([src/backend/extensions.ts](../src/backend/extensions.ts)) exposes:
- **Layer registration** for first-class additions:
- `extension.registerClient(name, ClientClass)`
- `extension.registerStore(name, StoreClass)`
- `extension.registerService(name, ServiceClass)`
- `extension.registerDriver(name, DriverClass)`
- `extension.registerController(name, ControllerClass)`
- **Lightweight wrappers** for the common case where a full class isn't worth it:
- `extension.on(event, handler)` — subscribe to event-bus events.
- `extension.get(path, opts?, handler)` / `.post` / `.put` / `.delete` / `.patch` / `.head` / `.options` / `.all` / `.use` — register routes. The `opts` shape is the same `RouteOptions` controllers use, so `subdomain`, `requireAuth`, `adminOnly`, body parsers, etc. all work identically.
- **Cross-layer access**: `extension.import('client' | 'store' | 'service' | 'controller' | 'driver')` returns a lazy proxy to instantiated objects, and `extension.config` exposes the live config.
```ts
import { extension } from '@heyputer/backend/src/extensions';
const services = extension.import('service');
extension.get('/healthcheck/deep', { subdomain: 'api', adminOnly: true }, async (_req, res) => {
res.json({ ok: await services.health.runDeepCheck() });
});
extension.on('user.signup', (_key, data) => {
console.log('new user', data.user.username);
});
```
## Conventions
- **TypeScript preferred** in new code where feasible. Existing JS is fine; convert opportunistically when you're already touching a file.
- **`camelCase`** for variable/function names; **`PascalCase`** for classes and for files that contain a class (`AuthService.ts`, `KVStoreDriver.ts`).
- **Deduplicate**. If two services need the same logic, lift it into a util/helper rather than calling sideways across the same layer — services should not depend on other services for code reuse.
- **Don't reach across layers.** Controllers do not poke clients directly; services do not register routes. If you find yourself wanting to, that's usually a signal the abstraction is wrong.
-33
View File
@@ -1,33 +0,0 @@
# Comment Prefixes
Comments have prefixes using
[Conventional: Comments](https://conventionalcomments.org/)
as a **loose** guideline, and using this markdown file as a
the actual guideline.
This document will be updated on an _as-needed_ basis.
## The rules
- A comment line always looks like this:
- A whitespace character
- Optional prefix matching `/[a-z-]+\([a-z-]a+\):/`
- A whitespace character
- The comment
- Formalized prefixes must follow the rules below
- Any other prefix can be used. After some uses it
might be good to formalize it, but that's not a hard rule.
## Formalized prefixes
- `todo:` is interchangable with the famous `TODO:`, **except:**
when lowercase (`todo:`) it can include a scope: `todo(security):`.
- `track:` is used to track common patterns.
- Anything written after `track:` must be registered in
[track-comments.md](../devmeta/track-comments.md)
- `wet:` is usesd to track anything that doesn't adhere
to the DRY principle; the following message should describe
where similar code is
- `compare(<identifier>):` is used to note differences between other
implementations of a similar idea
- `name:` pedantic commentary on the name of something
-105
View File
@@ -1,105 +0,0 @@
# Local Email Testing
This guide describes how to set up and use [MailHog](https://github.com/mailhog/MailHog) for local email testing in Puter development. MailHog provides a local email server that captures outgoing emails for testing purposes without actually sending them to real recipients.
## Setup
### 1. Configure Puter
Add the following configuration to your `volatile/config/config.json` file:
```json
"email": {
"host": "localhost",
"port": 1025
}
```
### 2. Install MailHog
Download and run MailHog on your local machine:
```bash
# Install MailHog
wget https://github.com/mailhog/MailHog/releases/download/v1.0.1/MailHog_linux_amd64
chmod +x MailHog_linux_amd64
./MailHog_linux_amd64
```
### 3. Install Nodemailer
Install Nodemailer to send test emails to the SMTP server:
```bash
npm install nodemailer
```
## Using MailHog
### Access Web Interface
Once MailHog is running, access the web interface at:
[http://127.0.0.1:8025/](http://127.0.0.1:8025/)
All captured emails and their recipients will be displayed in this interface.
### Testing Your MailHog Setup with Nodemailer
You can verify that your MailHog instance is working correctly by creating a simple test script using Nodemailer. This allows you to send test emails that will be captured by MailHog without actually delivering them to real recipients.
Here's a sample script you can use to test your MailHog setup:
```javascript
import nodemailer from "nodemailer";
// Configure transporter to use MailHog
const transporter = nodemailer.createTransport({
host: "localhost", // MailHog SMTP server address
port: 1025, // Default MailHog SMTP port
secure: false // No SSL/TLS required for MailHog
});
// Define a test email
const mailOptions = {
from: "[email protected]",
to: "[email protected]",
subject: "Hello from Nodemailer!",
text: "This is a test email sent using Nodemailer."
};
// Send the test email
transporter.sendMail(mailOptions)
.then(info => console.log("Email sent:", info.response))
.catch(error => console.error("Error:", error));
```
After sending an email with this script, you can view it in the MailHog web interface:
### How Puter Uses Nodemailer
Puter itself uses Nodemailer for sending emails through its `EmailService` class located in `/src/backend/src/services/EmailService.js`. This service handles various email templates for:
- Account verification
- Password recovery
- Two-factor authentication notifications
- File sharing notifications
- App approval notifications
- And more
The service creates a Nodemailer transport using the configuration from your `config.json` file, which is why setting up MailHog correctly is important for testing Puter's email functionality during development.
<img src="image.png" alt="Email in MailHog interface" width="300" height="200">
## Troubleshooting
If you encounter issues with MailHog:
1. Check if MailHog is running:
```bash
ps aux | grep MailHog
```
2. Ensure the correct port configurations in both MailHog and your application.
3. Check for any error messages in the MailHog console output.
-38
View File
@@ -1,38 +0,0 @@
# Puter Extensions
## Quickstart
Create and edit this file: `mods/mods_enabled/hello-puter.js`
```javascript
const { UserActorType, AppUnderUserActorType } = use.core;
extension.get('/hello-puter', (req, res) => {
const actor = req.actor;
let who = 'unknown';
if ( actor.type instanceof UserActorType ) {
who = actor.type.user.username;
}
if ( actor.type instanceof AppUnderUserActorType ) {
who = actor.type.app.name + ' on behalf of ' + actor.type.user.username;
}
res.send(`Hello, ${who}!`);
});
```
## Events
//
This is subject to change as we make efforts to simplify the process.
### Step 1: Configure a Mod Directory
Add this to your config:
```json
"mod_directories": [
"{source}/../mods/mods_available"
]
```
This adds the `mods/mods_available` directory to this
-89
View File
@@ -1,89 +0,0 @@
# Puter Extensions
## Quickstart
Create and edit this file: `mods/mods_enabled/hello-puter.js`
```javascript
// You can get definitions exposed by Puter via `use`
const { UserActorType, AppUnderUserActorType } = use.core;
// Endpoints can be registered directly on an extension
extension.get('/hello-puter', (req, res) => {
const actor = req.actor;
// Make a string "who" which says:
// "<username>", or:
// "<app> acting on behalf of <username>"
let who = 'unknown';
if ( actor.type instanceof UserActorType ) {
who = actor.type.user.username;
}
if ( actor.type instanceof AppUnderUserActorType ) {
who = actor.type.app.name
+ ' on behalf of '
+ actor.type.user.username;
}
res.send(`Hello, ${who}!`);
});
// Extensions can listen to events and manipulate Puter's behavior
extension.on('core.email.validate', event => {
if ( event.email.includes('evil') ) {
event.allow = false;
}
});
```
### Scope of `extension` and `use`
It is important to know that the `extension` global is temporary and does not
exist after your extension is loaded. If you wish to access the extension
object within a callback you will need to first bind it to a variable in
your extension's scope.
```javascript
const ext = extension;
extension.on('some-event', () => {
// This would throw an error
// extension.something();
// This works
ext.example();
})
```
The same is true for `use`. Calls to `use` should happen at the top of
the file, just like imports in ES6.
## Database Access
A database access object is provided to the extension via `extension.db`.
You **must** scope `extension` to another variable (`ext` in this example)
in order to access `db` from callbacks.
```javascript
const ext = extension;
extension.get('/user-count', { noauth: true, mw: [] }, (req, res) => {
const [count] = await ext.db.read(
'SELECT COUNT(*) as c FROM `user`'
);
});
```
The database access object has the following methods:
- `read(query, params)` - read from the database using a prepared statement. If read-replicas are enabled, this will use a replica.
- `write(query, params)` - write to the database using a prepared statement. If read-replicas are enabled, this will write to the primary.
- `pread(query, params)` - read from the database using a prepared statement. If read-replicas are enabled, this will read from the primary.
- `requireRead(query, params)` - read from the database using a prepared statement. If read-replicas are enabled, this will try reading from the replica first. If there are no results, a second attempt will be made on the primary.
## Events
See [events.md](./events.md)
## Definitions
See [definitions.md](./definitions.md)
@@ -1,14 +0,0 @@
## Definitions
### `core.config` - Configuration
Puter's configuration object. This includes values from `config.json` or their
defaults, and computed values like `origin` and `api_origin`.
```javascript
const config = use('core.config');
extension.get('/get-origin', { noauth: true }, (req, res) => {
res.send(config.origin);
})
```
-816
View File
@@ -1,816 +0,0 @@
export default [
{
properties: {
completionId: {
type: 'any',
mutability: 'mutable',
summary: 'completionId',
notes: [],
},
allow: {
type: 'boolean',
mutability: 'mutable',
summary: 'whether the operation is allowed',
notes: [],
},
intended_service: {
type: 'any',
mutability: 'mutable',
summary: 'intended service',
notes: [],
},
parameters: {
type: 'any',
mutability: 'mutable',
summary: 'parameters',
notes: [],
},
},
},
{
id: 'ai.prompt.complete',
description: `
This event is emitted for ai prompt complete operations.
`,
properties: {
intended_service: {
type: 'any',
mutability: 'mutable',
summary: 'intended service',
notes: [],
},
parameters: {
type: 'any',
mutability: 'mutable',
summary: 'parameters',
notes: [],
},
result: {
type: 'any',
mutability: 'mutable',
summary: 'result',
notes: [],
},
model_used: {
type: 'any',
mutability: 'mutable',
summary: 'model used',
notes: [],
},
service_used: {
type: 'any',
mutability: 'mutable',
summary: 'service used',
notes: [],
},
},
},
{
id: 'ai.prompt.cost-calculated',
description: `
This event is emitted for ai prompt cost calculated operations.
`,
},
{
id: 'ai.prompt.validate',
description: `
This event is emitted when a validate is being validated.
The event can be used to block certain validates from being validated.
`,
properties: {
completionId: {
type: 'any',
mutability: 'mutable',
summary: 'completionId',
notes: [],
},
allow: {
type: 'boolean',
mutability: 'mutable',
summary: 'whether the operation is allowed',
notes: [
'If set to false, the ai will be considered invalid.',
],
},
intended_service: {
type: 'any',
mutability: 'mutable',
summary: 'intended service',
notes: [],
},
parameters: {
type: 'any',
mutability: 'mutable',
summary: 'parameters',
notes: [],
},
},
},
{
id: 'app.new-icon',
description: `
This event is emitted for app new icon operations.
`,
properties: {
data_url: {
type: 'any',
mutability: 'no-effect',
summary: 'data url',
notes: [],
},
},
},
{
id: 'app.rename',
description: `
This event is emitted for app rename operations.
`,
properties: {
data_url: {
type: 'any',
mutability: 'no-effect',
summary: 'data url',
notes: [],
},
},
},
{
id: 'apps.invalidate',
description: `
This event is emitted when a invalidate is being validated.
The event can be used to block certain invalidates from being validated.
`,
properties: {
apps: {
type: 'any',
mutability: 'no-effect',
summary: 'apps',
notes: [],
},
},
},
{
id: 'captcha.check',
description: `
This event is emitted for captcha check operations.
`,
properties: {
required: {
type: 'any',
mutability: 'no-effect',
summary: 'required',
notes: [],
},
},
},
{
id: 'core.email.validate',
description: `
This event is emitted when an email is being validated.
The event can be used to block certain emails from being validated.
`,
properties: {
email: {
type: 'string',
mutability: 'no-effect',
summary: 'the email being validated',
notes: [
'The email may have already been cleaned.',
],
},
allow: {
type: 'boolean',
mutability: 'mutable',
summary: 'whether the email is allowed',
notes: [
'If set to false, the email will be considered invalid.',
],
},
},
},
{
id: 'core.fs.create.directory',
description: `
This event is emitted when a directory is created.
`,
properties: {
node: {
type: 'FSNodeContext',
mutability: 'no-effect',
summary: 'the directory that was created',
},
context: {
type: 'Context',
mutability: 'no-effect',
summary: 'current context',
},
},
},
{
id: 'core.request.measured',
description: `
This event is emitted when a requests incoming and outgoing bytes
have been measured.
`,
example: {
language: 'javascript',
code: /*javascript*/`
extension.on('core.request.measured', data => {
const measurements = data.measurements;
// measurements = { sz_incoming: integer, sz_outgoing: integer }
const actor = data.actor; // instance of Actor
console.log('\x1B[36;1m === MEASUREMENT ===\x1B[0m\n', {
actor: data.actor.uid,
measurements: data.measurements
});
});
`,
},
},
{
id: 'credit.check-available',
description: `
This event is emitted for credit check available operations.
`,
properties: {
available: {
type: 'any',
mutability: 'no-effect',
summary: 'available',
notes: [],
},
cost_uuid: {
type: 'string',
mutability: 'no-effect',
summary: 'cost uuid',
notes: [],
},
},
},
{
id: 'credit.funding-update',
description: `
This event is emitted when a funding-update is updated.
`,
properties: {
available: {
type: 'any',
mutability: 'no-effect',
summary: 'available',
notes: [],
},
cost_uuid: {
type: 'string',
mutability: 'no-effect',
summary: 'cost uuid',
notes: [],
},
},
},
{
id: 'credit.record-cost',
description: `
This event is emitted for credit record cost operations.
`,
properties: {
available: {
type: 'any',
mutability: 'no-effect',
summary: 'available',
notes: [],
},
cost_uuid: {
type: 'string',
mutability: 'no-effect',
summary: 'cost uuid',
notes: [],
},
},
},
{
id: 'driver.create-call-context',
description: `
This event is emitted when a create-call-context is created.
`,
properties: {
usages: {
type: 'any',
mutability: 'no-effect',
summary: 'usages',
notes: [],
},
},
},
{
id: 'email.validate',
description: `
This event is emitted when a validate is being validated.
The event can be used to block certain validates from being validated.
`,
properties: {
allow: {
type: 'boolean',
mutability: 'mutable',
summary: 'whether the operation is allowed',
notes: [
'If set to false, the email will be considered invalid.',
],
},
email: {
type: 'any',
mutability: 'mutable',
summary: 'email',
notes: [
'The email may have already been cleaned.',
],
},
},
},
{
id: 'fs.create.directory',
description: `
This event is emitted when a directory is created.
`,
},
{
id: 'fs.create.file',
description: `
This event is emitted when a file is created.
`,
properties: {
context: {
type: 'Context',
mutability: 'no-effect',
summary: 'current context',
notes: [],
},
},
},
{
id: 'fs.create.shortcut',
description: `
This event is emitted when a shortcut is created.
`,
},
{
id: 'fs.create.symlink',
description: `
This event is emitted when a symlink is created.
`,
},
{
id: 'fs.move.file',
description: `
This event is emitted for fs move file operations.
`,
properties: {
moved: {
type: 'any',
mutability: 'no-effect',
summary: 'moved',
notes: [],
},
old_path: {
type: 'string',
mutability: 'no-effect',
summary: 'path to the affected resource',
notes: [],
},
},
},
{
id: 'fs.pending.file',
description: `
This event is emitted for fs pending file operations.
`,
},
{
id: 'fs.storage.progress.copy',
description: `
This event reports progress of a copy operation.
`,
properties: {
context: {
type: 'Context',
mutability: 'no-effect',
summary: 'current context',
notes: [],
},
meta: {
type: 'object',
mutability: 'no-effect',
summary: 'additional metadata for the operation',
notes: [],
},
item_path: {
type: 'string',
mutability: 'no-effect',
summary: 'path to the affected resource',
notes: [],
},
},
},
{
id: 'fs.storage.upload-progress',
description: `
This event reports progress of a upload-progress operation.
`,
},
{
id: 'fs.write.file',
description: `
This event is emitted when a file is updated.
`,
properties: {
context: {
type: 'Context',
mutability: 'no-effect',
summary: 'current context',
notes: [],
},
},
},
{
id: 'ip.validate',
description: `
This event is emitted when a validate is being validated.
The event can be used to block certain validates from being validated.
`,
properties: {
res: {
type: 'any',
mutability: 'mutable',
summary: 'res',
notes: [],
},
end_: {
type: 'any',
mutability: 'mutable',
summary: 'end ',
notes: [],
},
end: {
type: 'any',
mutability: 'mutable',
summary: 'end',
notes: [],
},
},
},
{
id: 'outer.fs.write-hash',
description: `
This event is emitted when a write-hash is updated.
`,
properties: {
uuid: {
type: 'string',
mutability: 'no-effect',
summary: 'uuid',
notes: [],
},
},
},
{
id: 'outer.gui.item.added',
description: `
This event is emitted for outer gui item added operations.
`,
properties: {
response: {
type: 'any',
mutability: 'no-effect',
summary: 'response',
notes: [],
},
},
},
{
id: 'outer.gui.item.moved',
description: `
This event is emitted for outer gui item moved operations.
`,
properties: {
response: {
type: 'any',
mutability: 'no-effect',
summary: 'response',
notes: [],
},
},
},
{
id: 'outer.gui.item.pending',
description: `
This event is emitted for outer gui item pending operations.
`,
properties: {
response: {
type: 'any',
mutability: 'no-effect',
summary: 'response',
notes: [],
},
},
},
{
id: 'outer.gui.item.updated',
description: `
This event is emitted when a updated is updated.
`,
properties: {
response: {
type: 'any',
mutability: 'no-effect',
summary: 'response',
notes: [],
},
},
},
{
id: 'outer.gui.notif.ack',
description: `
This event is emitted for outer gui notif ack operations.
`,
properties: {
response: {
type: 'any',
mutability: 'no-effect',
summary: 'response',
notes: [],
},
},
},
{
id: 'outer.gui.notif.message',
description: `
This event is emitted for outer gui notif message operations.
`,
properties: {
response: {
type: 'any',
mutability: 'no-effect',
summary: 'response',
notes: [],
},
notification: {
type: 'any',
mutability: 'no-effect',
summary: 'notification',
notes: [],
},
},
},
{
id: 'outer.gui.notif.persisted',
description: `
This event is emitted for outer gui notif persisted operations.
`,
properties: {
response: {
type: 'any',
mutability: 'no-effect',
summary: 'response',
notes: [],
},
},
},
{
id: 'outer.gui.notif.unreads',
description: `
This event is emitted for outer gui notif unreads operations.
`,
properties: {
response: {
type: 'any',
mutability: 'no-effect',
summary: 'response',
notes: [],
},
},
},
{
id: 'outer.gui.submission.done',
description: `
This event is emitted for outer gui submission done operations.
`,
properties: {
response: {
type: 'any',
mutability: 'no-effect',
summary: 'response',
notes: [],
},
},
},
{
id: 'outer.gui.usage.update',
description: `
This event is emitted when a update is updated.
`,
},
{
id: 'outer.thread.notify-subscribers',
description: `
This event is emitted for outer thread notify subscribers operations.
`,
properties: {
uid: {
type: 'string',
mutability: 'no-effect',
summary: 'uid',
notes: [],
},
action: {
type: 'any',
mutability: 'no-effect',
summary: 'action',
notes: [],
},
data: {
type: 'any',
mutability: 'no-effect',
summary: 'data',
notes: [],
},
},
},
{
id: 'puter.signup',
description: `
This event is emitted for puter signup operations.
`,
properties: {
ip: {
type: 'any',
mutability: 'mutable',
summary: 'ip',
notes: [],
},
user_agent: {
type: 'any',
mutability: 'mutable',
summary: 'user agent',
notes: [],
},
body: {
type: 'any',
mutability: 'mutable',
summary: 'body',
notes: [],
},
},
},
{
id: 'request.measured',
description: `
This event is emitted for request measured operations.
`,
properties: {
req: {
type: 'any',
mutability: 'no-effect',
summary: 'req',
notes: [],
},
res: {
type: 'any',
mutability: 'no-effect',
summary: 'res',
notes: [],
},
},
},
{
id: 'request.will-be-handled',
description: `
This event is emitted for request will be handled operations.
`,
properties: {
res: {
type: 'any',
mutability: 'mutable',
summary: 'res',
notes: [],
},
end_: {
type: 'any',
mutability: 'mutable',
summary: 'end ',
notes: [],
},
end: {
type: 'any',
mutability: 'mutable',
summary: 'end',
notes: [],
},
},
},
{
id: 'template-service.hello',
description: `
This event is emitted for template-service hello operations.
`,
},
{
id: 'usages.query',
description: `
This event is emitted for usages query operations.
`,
properties: {
usages: {
type: 'any',
mutability: 'no-effect',
summary: 'usages',
notes: [],
},
},
},
{
id: 'user.email-changed',
description: `
This event is emitted for user email changed operations.
`,
properties: {
new_email: {
type: 'any',
mutability: 'no-effect',
summary: 'new email',
notes: [],
},
},
},
{
id: 'user.email-confirmed',
description: `
This event is emitted for user email confirmed operations.
`,
properties: {
email: {
type: 'any',
mutability: 'no-effect',
summary: 'email',
notes: [],
},
},
},
{
id: 'user.save_account',
description: `
This event is emitted for user save_account operations.
`,
properties: {
user: {
type: 'User',
mutability: 'no-effect',
summary: 'user associated with the operation',
notes: [],
},
},
},
{
id: 'web.socket.connected',
description: `
This event is emitted for web socket connected operations.
`,
properties: {
user: {
type: 'User',
mutability: 'mutable',
summary: 'user associated with the operation',
notes: [],
},
},
},
{
id: 'web.socket.user-connected',
description: `
This event is emitted for web socket user connected operations.
`,
properties: {
user: {
type: 'User',
mutability: 'mutable',
summary: 'user associated with the operation',
notes: [],
},
},
},
{
id: 'wisp.get-policy',
description: `
This event is emitted for wisp get policy operations.
`,
properties: {
policy: {
type: 'Policy',
mutability: 'mutable',
summary: 'policy information for the operation',
notes: [],
},
},
},
];
-761
View File
@@ -1,761 +0,0 @@
#### Property `completionId`
completionId
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `allow`
whether the operation is allowed
- **Type**: boolean
- **Mutability**: mutable
- **Notes**:
#### Property `intended_service`
intended service
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `parameters`
parameters
- **Type**: any
- **Mutability**: mutable
- **Notes**:
### `ai.prompt.complete`
This event is emitted for ai prompt complete operations.
#### Property `intended_service`
intended service
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `parameters`
parameters
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `result`
result
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `model_used`
model used
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `service_used`
service used
- **Type**: any
- **Mutability**: mutable
- **Notes**:
### `ai.prompt.cost-calculated`
This event is emitted for ai prompt cost calculated operations.
### `ai.prompt.validate`
This event is emitted when a validate is being validated.
The event can be used to block certain validates from being validated.
#### Property `completionId`
completionId
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `allow`
whether the operation is allowed
- **Type**: boolean
- **Mutability**: mutable
- **Notes**:
- If set to false, the ai will be considered invalid.
#### Property `intended_service`
intended service
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `parameters`
parameters
- **Type**: any
- **Mutability**: mutable
- **Notes**:
### `app.new-icon`
This event is emitted for app new icon operations.
#### Property `data_url`
data url
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `app.rename`
This event is emitted for app rename operations.
#### Property `data_url`
data url
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `apps.invalidate`
This event is emitted when a invalidate is being validated.
The event can be used to block certain invalidates from being validated.
#### Property `apps`
apps
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `captcha.check`
This event is emitted for captcha check operations.
#### Property `required`
required
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `core.email.validate`
This event is emitted when an email is being validated.
The event can be used to block certain emails from being validated.
#### Property `email`
the email being validated
- **Type**: string
- **Mutability**: no-effect
- **Notes**:
- The email may have already been cleaned.
#### Property `allow`
whether the email is allowed
- **Type**: boolean
- **Mutability**: mutable
- **Notes**:
- If set to false, the email will be considered invalid.
### `core.fs.create.directory`
This event is emitted when a directory is created.
#### Property `node`
the directory that was created
- **Type**: FSNodeContext
- **Mutability**: no-effect
#### Property `context`
current context
- **Type**: Context
- **Mutability**: no-effect
### `core.request.measured`
This event is emitted when a requests incoming and outgoing bytes
have been measured.
#### Example
```javascript
extension.on('core.request.measured', data => {
const measurements = data.measurements;
// measurements = { sz_incoming: integer, sz_outgoing: integer }
const actor = data.actor; // instance of Actor
console.log(' === MEASUREMENT ===
', {
actor: data.actor.uid,
measurements: data.measurements
});
});
```
### `credit.check-available`
This event is emitted for credit check available operations.
#### Property `available`
available
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
#### Property `cost_uuid`
cost uuid
- **Type**: string
- **Mutability**: no-effect
- **Notes**:
### `credit.funding-update`
This event is emitted when a funding-update is updated.
#### Property `available`
available
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
#### Property `cost_uuid`
cost uuid
- **Type**: string
- **Mutability**: no-effect
- **Notes**:
### `credit.record-cost`
This event is emitted for credit record cost operations.
#### Property `available`
available
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
#### Property `cost_uuid`
cost uuid
- **Type**: string
- **Mutability**: no-effect
- **Notes**:
### `driver.create-call-context`
This event is emitted when a create-call-context is created.
#### Property `usages`
usages
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `email.validate`
This event is emitted when a validate is being validated.
The event can be used to block certain validates from being validated.
#### Property `allow`
whether the operation is allowed
- **Type**: boolean
- **Mutability**: mutable
- **Notes**:
- If set to false, the email will be considered invalid.
#### Property `email`
email
- **Type**: any
- **Mutability**: mutable
- **Notes**:
- The email may have already been cleaned.
### `fs.create.directory`
This event is emitted when a directory is created.
### `fs.create.file`
This event is emitted when a file is created.
#### Property `context`
current context
- **Type**: Context
- **Mutability**: no-effect
- **Notes**:
### `fs.create.shortcut`
This event is emitted when a shortcut is created.
### `fs.create.symlink`
This event is emitted when a symlink is created.
### `fs.move.file`
This event is emitted for fs move file operations.
#### Property `moved`
moved
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
#### Property `old_path`
path to the affected resource
- **Type**: string
- **Mutability**: no-effect
- **Notes**:
### `fs.pending.file`
This event is emitted for fs pending file operations.
### `fs.storage.progress.copy`
This event reports progress of a copy operation.
#### Property `context`
current context
- **Type**: Context
- **Mutability**: no-effect
- **Notes**:
#### Property `meta`
additional metadata for the operation
- **Type**: object
- **Mutability**: no-effect
- **Notes**:
#### Property `item_path`
path to the affected resource
- **Type**: string
- **Mutability**: no-effect
- **Notes**:
### `fs.storage.upload-progress`
This event reports progress of a upload-progress operation.
### `fs.write.file`
This event is emitted when a file is updated.
#### Property `context`
current context
- **Type**: Context
- **Mutability**: no-effect
- **Notes**:
### `ip.validate`
This event is emitted when a validate is being validated.
The event can be used to block certain validates from being validated.
#### Property `res`
res
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `end_`
end
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `end`
end
- **Type**: any
- **Mutability**: mutable
- **Notes**:
### `outer.fs.write-hash`
This event is emitted when a write-hash is updated.
#### Property `uuid`
uuid
- **Type**: string
- **Mutability**: no-effect
- **Notes**:
### `outer.gui.item.added`
This event is emitted for outer gui item added operations.
#### Property `response`
response
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `outer.gui.item.moved`
This event is emitted for outer gui item moved operations.
#### Property `response`
response
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `outer.gui.item.pending`
This event is emitted for outer gui item pending operations.
#### Property `response`
response
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `outer.gui.item.updated`
This event is emitted when a updated is updated.
#### Property `response`
response
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `outer.gui.notif.ack`
This event is emitted for outer gui notif ack operations.
#### Property `response`
response
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `outer.gui.notif.message`
This event is emitted for outer gui notif message operations.
#### Property `response`
response
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
#### Property `notification`
notification
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `outer.gui.notif.persisted`
This event is emitted for outer gui notif persisted operations.
#### Property `response`
response
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `outer.gui.notif.unreads`
This event is emitted for outer gui notif unreads operations.
#### Property `response`
response
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `outer.gui.submission.done`
This event is emitted for outer gui submission done operations.
#### Property `response`
response
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `outer.gui.usage.update`
This event is emitted when a update is updated.
### `outer.thread.notify-subscribers`
This event is emitted for outer thread notify subscribers operations.
#### Property `uid`
uid
- **Type**: string
- **Mutability**: no-effect
- **Notes**:
#### Property `action`
action
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
#### Property `data`
data
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `puter.signup`
This event is emitted for puter signup operations.
#### Property `ip`
ip
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `user_agent`
user agent
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `body`
body
- **Type**: any
- **Mutability**: mutable
- **Notes**:
### `request.measured`
This event is emitted for request measured operations.
#### Property `req`
req
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
#### Property `res`
res
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `request.will-be-handled`
This event is emitted for request will be handled operations.
#### Property `res`
res
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `end_`
end
- **Type**: any
- **Mutability**: mutable
- **Notes**:
#### Property `end`
end
- **Type**: any
- **Mutability**: mutable
- **Notes**:
### `sns`
This event is emitted for sns operations.
#### Property `message`
message
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `template-service.hello`
This event is emitted for template-service hello operations.
### `usages.query`
This event is emitted for usages query operations.
#### Property `usages`
usages
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `user.email-changed`
This event is emitted for user email changed operations.
#### Property `new_email`
new email
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `user.email-confirmed`
This event is emitted for user email confirmed operations.
#### Property `email`
email
- **Type**: any
- **Mutability**: no-effect
- **Notes**:
### `user.save_account`
This event is emitted for user save_account operations.
#### Property `user`
user associated with the operation
- **Type**: User
- **Mutability**: no-effect
- **Notes**:
### `web.socket.connected`
This event is emitted for web socket connected operations.
#### Property `user`
user associated with the operation
- **Type**: User
- **Mutability**: mutable
- **Notes**:
### `web.socket.user-connected`
This event is emitted for web socket user connected operations.
#### Property `user`
user associated with the operation
- **Type**: User
- **Mutability**: mutable
- **Notes**:
### `wisp.get-policy`
This event is emitted for wisp get policy operations.
#### Property `policy`
policy information for the operation
- **Type**: Policy
- **Mutability**: mutable
- **Notes**:
-38
View File
@@ -1,38 +0,0 @@
import dedent from 'dedent';
import events from './events.json.js';
const mdlib = {};
mdlib.h = (out, n, str) => {
out(`${'#'.repeat(n)} ${str}\n\n`);
};
const N_START = 3;
const out = str => process.stdout.write(str);
for ( const event of events ) {
mdlib.h(out, N_START, `\`${event.id}\``);
out(`${dedent(event.description) }\n\n`);
for ( const k in event.properties ) {
const prop = event.properties[k];
mdlib.h(out, N_START + 1, `Property \`${k}\``);
out(`${prop.summary }\n`);
out(`- **Type**: ${prop.type}\n`);
out(`- **Mutability**: ${prop.mutability}\n`);
if ( prop.notes ) {
out('- **Notes**:\n');
for ( const note of prop.notes ) {
out(` - ${note}\n`);
}
}
out('\n');
}
if ( event.example ) {
mdlib.h(out, N_START + 1, 'Example');
out(`\`\`\`${event.example.language}\n${dedent(event.example.code)}\n\`\`\`\n`);
}
out('\n');
}
@@ -1,68 +0,0 @@
export default [
{
id: 'core.email.validate',
description: `
This event is emitted when an email is being validated.
The event can be used to block certain emails from being validated.
`,
properties: {
email: {
type: 'string',
mutability: 'no-effect',
summary: 'the email being validated',
notes: [
'The email may have already been cleaned.',
],
},
allow: {
type: 'boolean',
mutability: 'mutable',
summary: 'whether the email is allowed',
notes: [
'If set to false, the email will be considered invalid.',
],
},
},
},
{
id: 'core.request.measured',
description: `
This event is emitted when a requests incoming and outgoing bytes
have been measured.
`,
example: {
language: 'javascript',
code: /*javascript*/`
extension.on('core.request.measured', data => {
const measurements = data.measurements;
// measurements = { sz_incoming: integer, sz_outgoing: integer }
const actor = data.actor; // instance of Actor
console.log('\\x1B[36;1m === MEASUREMENT ===\\x1B[0m\\n', {
actor: data.actor.uid,
measurements: data.measurements
});
});
`,
},
},
{
id: 'core.fs.create.directory',
description: `
This event is emitted when a directory is created.
`,
properties: {
node: {
type: 'FSNodeContext',
mutability: 'no-effect',
summary: 'the directory that was created',
},
context: {
type: 'Context',
mutability: 'no-effect',
summary: 'current context',
},
},
},
];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

-64
View File
@@ -1,64 +0,0 @@
# Repository Structure and Tooling
Puter has many of its parts in a single [monorepo](https://en.wikipedia.org/wiki/Monorepo),
rather than a single repository for each cohesive part.
We feel this makes it easier for new contributors to develop Puter since you don't
need to figure out how to tie the parts together or how to work with Git submodules.
It also makes it easier for us to maintain project-wide conventions and tooling.
Some tools, like [puter-cli](https://github.com/HeyPuter/puter-cli), exist in separate
repositories. The `puter-cli` tool is used externally and can communicate with Puter's
API on our production (puter.com) instance or your own instance of Puter, so there's
not really any advantage to putting it in the monorepo.
## Top-Level directories
### The `doc` directory
The top-level `doc` directory contains the file you're reading right now.
Its scope is documentation for using and contributing to Puter in general,
and linking to more specific documentation in other places.
All `doc` directories will have a `README.md` which should be considered as
the index file for the documentation. All documentation under a `doc`
directory should be accessible via a path of links starting from `README.md`.
### The `src` directory
Every directory under `/tools` is [an npm "workspaces" module](https://docs.npmjs.com/cli/v8/using-npm/workspaces). Every direct child of this directory (generally) has a `package.json` and a `src` directory.
Some of these modules are core pieces of Puter:
- **Puter's backend** is [`/src/backend`](/src/backend)
- See [key locations in backend documentation](/src/backend/doc/contributors/structure.md)
- **Puter's GUI** is [`/src/gui`](/src/gui)
Some of these modules are apps:
- **Puter's Terminal**: [`/src/terminal`](/src/terminal)
- **Puter's Shell**: [`/src/phoenix`](/src/phoenix)
Some of these modules are libraries:
- **common javascript**: [`/src/putility`](/src/putility)
- **runtime import mechanism**: [`/src/useapi`](/src/useapi)
- **Puter's "puter.js" browser SDK**: [`/src/puter-js`](/src/puter-js)
### The `volatile` directory
When you're running Puter with development instructions (i.e. `npm start`),
Puter's configuration directory will be `volatile/config` and Puter's
runtime directory will be `volatile/runtime`, instead of the standard
`/etc/puter` and `/var/puter` directories in production installations.
We should probably rename this directory, actually, but it would inconvenience
a lot of people right now if we did.
### The `tools` directory
Every directory under `/tools` is [an npm "workspaces" module](https://docs.npmjs.com/cli/v8/using-npm/workspaces).
This is where `run-selfhosted.js` is. That's the entrypoint for `npm start`.
These tools are underdocumented and may not behave well if they're not executed
from the correct working directory (which is different for different tools).
Consider this a work-in-progress. If you want to use or contribute to anything
under this directory, for now you should
[tag @KernelDeimos on the community Discord](https://discord.gg/PQcx7Teh8u).
-2
View File
@@ -1,2 +0,0 @@
### `vscode`
- `es6-string-html`
-103
View File
@@ -1,103 +0,0 @@
## 2024-10-16
### Considerations for Mountpoints Feature
- `_storage_upload` takes paramter `uuid` instead of `path`
- S3 bucket strategy needs the UUID
- If we do hashes, 10MB chunks should be fine
- we're already able to smooth out bursty traffic using the
EWA algorithm
- Use of `systemFSEntryService`
- Is that normalized? Does everything go through this interface?
- Storage interface has methods like `post_insert`
- as far as I can tell this doesn't pose any issue
-
### Brainstorming Migration Strategies
#### Interface boundary at HL<->LL filesystem methods
-- **tags:** brainstorming
From the perspectice of a trait-oriented implementation,
which is not how LL/HL filesystem operations are currently implemented,
the LL-class operations are implemented in separate traits.
The composite trait containing all of these traits would be the trait
that represents a filesystem implementation itself.
Other filesystem interfaces that I've seen, such as FUSE and 9p,
all usually have a monolithic interface - that is to say, an interface
which includes all of the filesystem operations, rather than several
interfaces each implementing a single filesystem operaiton.
Something about the fact that the LL-class operations are in separate
classes makes it difficult to reason about how to move.
Is it simply that multiple files in a directory is just more
annoying to think about? Maybe, but there must be something more.
Perhaps it's that there are several references. Each implementation
(that is, implemenation of a single filesystem operation) could have
any number of different references across any number of different files.
This would not be the case with a monolithic interface.
I think the best of both worlds would be to have an interface representing
the entire filesystem and, in one place, link of of the individual
operation implementations to compose a filesystem implementation
### Filesystem Brainstorming
Puter's backend uses a service architecture. Each service is an instance
of a class extending "Service". A service can listen to events of the
backend's lifecycle, interact with other services, and interact with
external interfaces such as APIs and databases.
Puter's current filesystem, let's call it PuterFSv1, exists as the result
of multiple services working together. We have LocalDiskStorageService
which mimics an S3 bucket on a local system, and we have
DatabaseFSEntryService which manages information about files, directories,
and their relationships within the database, and therefore depends on
DatabaseAccessService.
It is now time to introduce a MountpointService. This will allow another
service or a user's configuration to assign an instance of a filesystem
implementation (such as PuterFSv1) to a specific path.
The trouble here is that PuterFSv1 is composed of services, and the nature
of a service is such that it exists for the lifecycle of the application.
The class for a particular service can be re-used and registered with
multiple names (creating multiple services with the same implementation
but perhaps different configuration), but that's only a clean scenario when
there is just one service. PuterFSv1, on the other hand, is like an
imaginary service composed of other services.
The following possibilities then should be discussed:
- CompositeService base class for a service that is composed of
more than one service.
- Refactor filesystem to not use service architecture.
- Each filesystem service can manage state and configuration
for multiple mountpoints
(I don't like this idea; it feels messy. I wonder what software
principles this violates)
We can take advantage of traits/interfaces here.
PuterFSv1 depends on two interfaces:
- An S3-like data storage implementation
- An fsentry storage implementation
Counterintuitively from what I first thought, "Refactor the filesystem"
actually looks like the best solution, and it doens't even look like it
will be that difficult. In fact, it'll likely make the filesystem easier
to maintain and more robust as a result.
Additionally, we can introduce PuterFSv2, which will introduce storing
data in chunks identified by their hashes, and associated hashes with
fsentries.
PuterFSService will be a new service which registers 'PuterFSv1' with
FilesystemService.
An instance of a filesystem needs to be separate from a mountpoint.
For example, PuterFSv1 will usually have only one instance but it may
be mounted several different times. `/some-user` on Puter's VFS could
be a mountpoint for `/some-user` in the instance of PuterFSv1.
-62
View File
@@ -1,62 +0,0 @@
# Track Comments
Comments beginning with `// track:`. See
[comment_prefixes.md](../contributors/comment_prefixes.md)
## Track Comment Registry
- `track: type check`:
A condition that's used to check the type of an imput.
- `track: adapt`
A value can by adapted from another type at this line.
- `track: bounds check`:
A condition that's used to check the bounds of an array
or other list-like entity.
- `track: ruleset`
A series of conditions that early-return or `continue`
- `track: object description in comment`
A comment above the creation of some object which
could potentially have a `description` property.
This is especially relevant if the object is stored
in some kind of registry where multiple objects
could be listed in the console.
- `track: slice a prefix`
A common pattern where a prefix string is "sliced off"
of another string to obtain a significant value, such
as an indentifier.
- `track: actor type`
The sub-type of an Actor object is checked.
- `track: scoping iife`
An immediately-invoked function expression specifically
used to reduce scope clutter.
- `track: good candidate for sequence`
Some code involves a series of similar steps,
or there's a common behavior that should happen
in between. The Sequence class is good for this so
it might be a worthy migration.
- `track: opposite condition of sibling`
A sibling class, function, method, or other construct of
source code has a boolean expression which always evaluates
to the opposite of the one below this track comment.
- `track: null check before processing`
An object could be undefined or null, additional processing
occurs after a null check, and the unprocessed object is not
relevant to the rest of the code. If the code for obtaining
the object and processing it is moved to a function outside,
then the null check should result in a early return of null;
this code with the track comment may have additional logic
for the null/undefined case.
- `track: manual safe object`
This code manually creates a new "client-safe" version of
some object that's in scope. This could be either to pass
onto the browser or to pass to something like the
notification service.
- `track: common operations on multiple items`
A patterm which emerges when multiple variables have
common operations done upon them in sequence.
It may be applicable to write an iterator in the
future, or something will come up that require
these to be handled with a modular approach instead.
- `track: checkpoint`
A location where some statement about the state of the
software must hold true.
-45
View File
@@ -1,45 +0,0 @@
# Meta Documentation
Guidelines for documentation.
## How documentation is organized
This documentation exists in the Puter repository.
You may be reading this on the GitHub wiki instead, which we generate
from the repository docs. These docs are always under a directory
named `doc/`.
From [./contributors/structure.md](./contributors/structure.md):
> The top-level `doc` directory contains the file you're reading right now.
> Its scope is documentation for using and contributing to Puter in general,
> and linking to more specific documentation in other places.
>
> All `doc` directories will have a `README.md` which should be considered as
> the index file for the documentation. All documentation under a `doc`
> directory should be accessible via a path of links starting from `README.md`.
### Documentation Structure
The top-level `doc` directory contains the following subdirectories:
- `api/` - API documentation for Puter services
- `contributors/` - Documentation for contributors to the Puter project
- `devmeta/` - Meta documentation for developers
- `i18n/` - Internationalization documentation
- `planning/` - Project planning documentation
- `self-hosters/` - Documentation for self-hosting Puter
- `uncategorized/` - Miscellaneous documentation
As well as some files:
- `README.md` - Documentation overview optimized for humans.
- `AI.md` - Documentation overview optimized for AI/LLM agents.
Module-specific documentation follows a similar structure, with each module having its own `doc` directory. For contributor-specific documentation within a module, use a `contributors` subdirectory within the module's `doc` directory.
## Docs Styleguide
### "is" and "is not"
- When "A is B", bold "is": "A **is** B" (`A **is** B`)
- When "A is not B", bold "not": "A is **not** B" (`A is **not** B`)
+1 -1
View File
@@ -113,7 +113,7 @@ Terhubung dengan maintainer dan komunitas melalui saluran-saluran berikut:
- Reddit: [reddit.com/r/puter/](https://www.reddit.com/r/puter/)
- Mastodon: [mastodon.social/@puter](https://mastodon.social/@puter)
- Isu keamanan? [[email protected]](mailto:[email protected])
- Email maintainers di [[email protected]](mailto:[email protected])
- Email pengelola di [[email protected]](mailto:[email protected])
Kami selalu senang membantu Anda dengan pertanyaan apa pun yang Anda miliki. Jangan ragu untuk bertanya!
+1 -1
View File
@@ -104,7 +104,7 @@ Puterは[**puter.com**](https://puter.com)でホストサービスとして利
メンテナーやコミュニティと以下のチャンネルを通じてつながりましょう:
- バグ報告や機能リクエストがありますか? [issueを開く](https://github.com/HeyPuter/puter/issues/new/choose) してください。
- バグ報告や機能リクエストは [新規Issue](https://github.com/HeyPuter/puter/issues/new/choose) まで
- Discord: [discord.com/invite/PQcx7Teh8u](https://discord.com/invite/PQcx7Teh8u)
- X (Twitter): [x.com/HeyPuter](https://x.com/HeyPuter)
- Reddit: [reddit.com/r/puter/](https://www.reddit.com/r/puter/)
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (C) 2024 Puter Technologies Inc.
Copyright (C) 2024-present Puter Technologies Inc.
This file is part of Puter.
-149
View File
@@ -1,149 +0,0 @@
# Puter in Production
## Building
```bash
npm run build
```
## Usage
Will build Puter in the `dist` directory. Include the generated `./dist/gui.js` file in your HTML page and call `gui()` when the page is loaded:
```html
<script type="text/javascript" src="./dist/gui.js"></script>
<script type="text/javascript">
window.addEventListener('load', function() {
// Initialize the GUI. All options are optional!
gui({
// The origin of the app. This is the base URL of the GUI.
gui_origin: "https://puter.com",
// The origin of the API. This is the base URL of the API endpoints that the GUI will call for all its operations.
api_origin: "https://api.puter.com",
// The domain under which user websites are hosted.
hosting_domain: "puter.site",
// The maximum length of file/directory names.
max_item_name_length: 500,
// If GUI has to enforce email verification before allowing user to publish a website.
require_email_verification_to_publish_website: true,
})
});
</script>
```
## Full Production Example
Assuming the following directory structure in production:
```
.
├── dist/
│ ├── favicons/
│ ├── images/
│ ├── bundle.min.css
│ ├── bundle.min.js
│ ├── gui.js
│ └── ...
└── index.html
```
The `index.html` file below will load Puter and all the necessary meta tags, favicons, and branding assets:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Puter</title>
<meta name="author" content="Puter Technologies Inc.">
<meta name="description" content="Puter is a privacy-first personal cloud to keep all your files, apps, and games in one private and secure place, accessible from anywhere at any time.">
<meta name="facebook-domain-verification" content="e29w3hjbnnnypf4kzk2cewcdaxym1y" />
<link rel="canonical" href="https://puter.com">
<!-- Meta meta tags -->
<meta property="og:url" content="https://puter.com">
<meta property="og:type" content="website">
<meta property="og:title" content="Puter">
<meta property="og:description" content="Puter is a privacy-first personal cloud to keep all your files, apps, and games in one private and secure place, accessible from anywhere at any time.">
<meta property="og:image" content="./dist/images/screenshot.png">
<!-- Twitter meta tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="puter.com">
<meta property="twitter:url" content="https://puter.com">
<meta name="twitter:title" content="Puter">
<meta name="twitter:description" content="Puter is a privacy-first personal cloud to keep all your files, apps, and games in one private and secure place, accessible from anywhere at any time.">
<meta name="twitter:image" content="./dist/images/screenshot.png">
<!-- favicons -->
<link rel="apple-touch-icon" sizes="57x57" href="./dist/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="./dist/favicons/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="./dist/favicons/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="./dist/favicons/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="./dist/favicons/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="./dist/favicons/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="./dist/favicons/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="./dist/favicons/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="./dist/favicons/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="./dist/favicons/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="./dist/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="./dist/favicons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="./dist/favicons/favicon-16x16.png">
<link rel="manifest" href="./dist/manifest.json">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="./dist/favicons/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<!-- Preload images when applicable -->
<link rel="preload" as="image" href="./dist/images/wallpaper.webp">
</head>
<body>
<!-- Load the GUI script -->
<script type="text/javascript" src="./dist/gui.js"></script>
<!-- Initialize GUI when document is loaded -->
<script type="text/javascript">
window.addEventListener('load', function() {
gui()
});
</script>
</body>
</html>
```
### Server settings
The GUI is a single page application (SPA) and as best practice any route under root (`/*`) should preferably load the `index.html` file. However, there are situations where we want to load a custom page for a specific route: for example, the `/privacy` route may need to load a page that contains your privacy policy and has nothing to do with the GUI application. In these cases it is ok to load a custom page as long as the following essential GUI routes are loaded with the GUI (i.e. `index.html` file):
- `/app/*`
- `/action/*`
In other words, consider the routes above as "reserved" for Puter.
### Publish My Website
Right-click anywhere on the desktop to display options
From the options menu, select "New".
Then, choose "Folder".
Give the folder a name according to your preference.
After creating the folder:
Right-click on the folder.
Select the option "Publish as Website".
### Best Practices
- The `title` tags and meta tags (`<title></title>`, `<meta property="og:title"`, `<meta name="twitter:title"`, ...) should be dynamically set by the server. For example, if the URL is of an app (e.g. `https://puter.com/app/editor`) the `title` tags and meta tags should contain the app's title rather than the generic Puter title.
- The `description` meta tags (`<meta name="description"`, `<meta property="og:description"`, `<meta name="twitter:description"`, ...) should be dynamically set by the server. For example, if the URL is of an app (e.g. `https://puter.com/app/editor`) the `description` meta tags should contain the app's description rather than the generic Puter description.
- Make sure to escape any HTML code that is dynamically added to the HTML page. For example, if the app's description is `Puter is a <b>privacy-first</b> personal cloud to keep all your files, apps, and games in one private and secure place, accessible from anywhere at any time.` the `<b>` tag should be escaped to `&lt;b&gt;` so that the browser doesn't interpret it as an HTML tag.
- Make sure to replace all new line characters with space when dynamically adding text to the HTML page.
- Generally, for UX and SEO reasons make sure that the tags are filled with relevant information about the state the URL is representing. E.g. is the user on the desktop or an app?
-84
View File
@@ -1,84 +0,0 @@
export default [
{
key: 'domain',
description: `
Domain name of the Puter instance. This may be used to generate URLs
in the UI. If "allow_all_host_values" is false or undefined, the domain
will be used to validate the host header of incoming requests.
`,
example_values: [
'example.com',
'subdomain.example.com',
],
},
{
key: 'protocol',
description: `
The protocol to use for URLs. This should be either "http" or "https".
`,
example_values: [
'http',
'https',
],
},
{
key: 'static_hosting_domain',
description: `
This domain name will be used for public site URLs. For example: when
you right-click a directory and choose "Publish as Website".
This domain should point to the same server. If you have a LAN configuration
you could set this to something like
\`site.192.168.555.12.nip.io\`, replacing
\`192.168.555.12\` with a valid IP address belonging to the server.
`,
},
{
key: 'allow_all_host_values',
description: `
If true, Puter will accept any host header value in incoming requests.
This is useful for development, but should be disabled in production.
`,
},
{
key: 'allow_nipio_domains',
description: `
If true, Puter will allow requests with host headers that end in nip.io.
This is useful for development, LAN, and VPN configurations.
`,
},
{
key: 'http_port',
description: `
The port to listen on for HTTP requests.
`,
},
{
key: 'enable_public_folders',
description: `
If true, any /username/Public directory will be available to all
users, including anonymous users.
`,
},
{
key: 'disable_temp_users',
description: `
If true, new users will see the login/signup page instead of being
automatically logged in as a temporary user.
`,
},
{
key: 'disable_user_signup',
description: `
If true, the signup page will be disabled and the backend will not
accept new user registrations.
`,
},
{
key: 'disable_fallback_mechanisms',
description: `
A general setting to prevent any fallback behavior that might
"hide" errors. It is recommended to set this to true when
debugging, testing, or developing new features.
`,
},
];
-81
View File
@@ -1,81 +0,0 @@
# Configuring Puter
## Terminology
- **root** - the "top level" of configuration; if a key-value pair is in/at "the root"
that means it is **not in a nested object**
(ex: values under "services" are **not** at the root).
## Config Locations
Running the server will generate a configuration file in one of these locations:
- `config/config.json` when [Using Docker](#using-docker)
- `volatile/config/config.json` in [Local Development](#local-development)
- `/etc/puter/config.json` on a server (or within a Docker container)
## Editing Configuration
For a list of all possible config values, see [config_values.md](./config_values.md)
Instead of editing the generated `config.json`, you can make a config file
that references it. This makes it easier to maintain if you frequently update
Puter, since you can then just delete `config.json` to get new defaults.
For example, a `local.json` might look like this:
```json
{
// Always include this header
"$version": "v1.1.0",
"$requires": [
"config.json"
],
"config_name": "local",
// Your custom configuration
"domain": "my-puter.example.com"
}
```
To use `local.json` instead of `config.json` you will need to set the
environment variable `PUTER_CONFIG_PROFILE=local` in the context where
you are running Puter.
## Sample Configuration
The default configuration generated by Puter will look
something like the following (updated 2025-02-26):
```json
{
"config_name": "generated default config",
"mod_directories": [
"{source}/../extensions"
],
"env": "dev",
"nginx_mode": true,
"server_id": "localhost",
"http_port": "auto",
"domain": "puter.localhost",
"protocol": "http",
"contact_email": "[email protected]",
"services": {
"database": {
"engine": "sqlite",
"path": "puter-database.sqlite"
},
"dynamo" :{"path":"./puter-ddb"}
},
"cookie_name": "...",
"jwt_secret": "...",
"url_signature_secret": "...",
"private_uid_secret": "...",
"private_uid_namespace": "...",
"": null
}
```
## Root-Level Parameters
- **domain** - origin for Puter. Do **not** include URL schema (the 'http(s)://' portion)
-
-72
View File
@@ -1,72 +0,0 @@
### `domain`
Domain name of the Puter instance. This may be used to generate URLs
in the UI. If "allow_all_host_values" is false or undefined, the domain
will be used to validate the host header of incoming requests.
#### Examples
- `"domain": "example.com"`
- `"domain": "subdomain.example.com"`
### `protocol`
The protocol to use for URLs. This should be either "http" or "https".
#### Examples
- `"protocol": "http"`
- `"protocol": "https"`
### `static_hosting_domain`
This domain name will be used for public site URLs. For example: when
you right-click a directory and choose "Publish as Website".
This domain should point to the same server. If you have a LAN configuration
you could set this to something like
`site.192.168.555.12.nip.io`, replacing
`192.168.555.12` with a valid IP address belonging to the server.
### `allow_all_host_values`
If true, Puter will accept any host header value in incoming requests.
This is useful for development, but should be disabled in production.
### `allow_nipio_domains`
If true, Puter will allow requests with host headers that end in nip.io.
This is useful for development, LAN, and VPN configurations.
### `http_port`
The port to listen on for HTTP requests.
### `enable_public_folders`
If true, any /username/Public directory will be available to all
users, including anonymous users.
### `disable_temp_users`
If true, new users will see the login/signup page instead of being
automatically logged in as a temporary user.
### `disable_user_signup`
If true, the signup page will be disabled and the backend will not
accept new user registrations.
### `disable_fallback_mechanisms`
A general setting to prevent any fallback behavior that might
"hide" errors. It is recommended to set this to true when
debugging, testing, or developing new features.
-85
View File
@@ -1,85 +0,0 @@
# Configuring Domains for Self-Hosted Puter
## Local Network Configuration
### Prerequisite Conditions
Ensure the hosting device has a static IP address to prevent potential connectivity issues due to IP changes. This setup will enable seamless access to Puter and its services across your local network.
### Using `nip.io`
We recommend this configuration for LAN setups. All you need to do is set the following
at root level in your configuration file:
```json
"allow_nipio_domains": true
```
Puter requires multiple origins to work correctly. `nip.io` is a wildcard DNS for IP addresses,
so Puter can still have multiple subdomains and you don't need to configure your own DNS or
hosts file.
### Using Hosts Files
The hosts file is a straightforward way to map domain names to IP addresses on individual devices. It's simple to set up but requires manual changes on each device that needs access to the domains.
#### Windows
1. Open Notepad as an administrator.
2. Open the file located at `C:\Windows\System32\drivers\etc\hosts`.
3. Add lines for your domain and subdomain with the server's IP address, in the
following format:
```
192.168.1.10 puter.local
192.168.1.10 api.puter.local
```
#### For macOS and Linux:
1. Open a terminal.
2. Edit the hosts file with a text editor, e.g., `sudo nano /etc/hosts`.
3. Add lines for your domain and subdomain with the server's IP address, in the
following format:
```
192.168.1.10 puter.local
192.168.1.10 api.puter.local
```
4. Save and exit the editor.
### Using Router Configuration
Some routers allow you to add custom DNS rules, letting you configure domain names network-wide without touching each device.
1. Access your router’s admin interface (usually through a web browser).
2. Look for DNS or DHCP settings.
3. Add custom DNS mappings for `puter.local` and `api.puter.local` to the hosting device's IP address.
4. Save the changes and reboot the router if necessary.
This method's availability and steps may vary depending on your router's model and firmware.
### Using Local DNS
Setting up a local DNS server on your network allows for flexible and scalable domain name resolution. This method works across all devices automatically once they're configured to use the DNS server.
#### Options for DNS Software:
- **Pi-hole**: Acts as both an ad-blocker and a DNS server. Ideal for easy setup and maintenance.
- **BIND9**: Offers comprehensive DNS server capabilities for complex setups.
- **dnsmasq**: Lightweight and suitable for smaller networks or those new to running a DNS server.
**contributors note:** feel free to add any software you're aware of
which might help with this to the list. Also, feel free to add instructions here for specific software; our goal is for Puter to be easy to setup with tools you're already familiar with.
#### General Steps:
1. Choose and install DNS server software on a device within your network.
2. Configure the DNS server to resolve `puter.local` and `api.puter.local` to the IP address of your Puter hosting device.
3. Update your router's DHCP settings to distribute the DNS server's IP address to all devices on the network.
By setting up a local DNS server, you gain the most flexibility and control over your network's domain name resolution, ensuring that all devices can access Puter and its API without manual configuration.
## Production Configuration
Please note the self-hosting feature is still in alpha and a public production
deployment is not recommended at this time. However, if you wish to host
publicly you can do so following the same steps you normally would to configure
a domain name and ensuring the `api` subdomain points to the server as well.
-80
View File
@@ -1,80 +0,0 @@
# First Run Issues
## "Cannot find package '@heyputer/backend'"
Scenario: You see the following output:
```
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Cannot find package '@heyputer/backend' ┃
┃ 📝 this usually happens if you forget `npm install` ┃
┃ Suggestions: ┃
┃ - try running `npm install` ┃
┃ Technical Notes: ┃
┃ - @heyputer/backend is in an npm workspace ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
```
1. Ensure you have run `npm install`.
2. [Install build essentials for your distro](#installing-build-essentials),
then run `npm install` again.
## Installing Build Essentials
### Debian-based distros
```
sudo apt update
sudo apt install build-essential
```
### RHEL-family distros (Fedora, Rocky, etc)
For distros using dnf5 (Fedora 41+):
```
sudo dnf install @development-tools
```
Otherwise:
```
sudo dnf groupinstall "Development Tools"
```
### "I use Arch btw"
```
sudo pacman -S base-devel
```
### Alpine
If you're running in Puter's Alpine image then this is already installed.
```
sudo apk add build-base
```
### Gentoo
You know what you're doing; you just wanted to see if we mentioned Gentoo.
## "Could not load the "sharp" module using the freebsd-x64 runtime"
In order to get it to work on FreeBSD, you will need to build sharp from source and link it to the project.
```
pkg install vips
git clone --depth=1 https://github.com/lovell/sharp.git
cd sharp
yarn install
sudo npm link
```
After `npm install` you can link the prebuilt module
```
# cd puter
# npm install
npm link sharp
npm start
```
-25
View File
@@ -1,25 +0,0 @@
import dedent from 'dedent';
import configVals from './config-vals.json.js';
const mdlib = {};
mdlib.h = (out, n, str) => {
out(`${'#'.repeat(n)} ${str}\n\n`);
};
const N_START = 3;
const out = str => process.stdout.write(str);
for ( const configVal of configVals ) {
mdlib.h(out, N_START, `\`${configVal.key}\``);
out(`${dedent(configVal.description) }\n\n`);
if ( configVal.example_values ) {
mdlib.h(out, N_START + 1, 'Examples');
for ( const example of configVal.example_values ) {
out(`- \`"${configVal.key}": ${JSON.stringify(example)}\`\n`);
}
}
out('\n');
}
-59
View File
@@ -1,59 +0,0 @@
# Self-Hosting Puter
> [!WARNING]
> The self-hosted version of Puter is currently in alpha stage and should not be used in production yet. It is under active development and may contain bugs, other issues. Please exercise caution and use it for testing and evaluation purposes only.
### Self-Hosting Differences
Currently, the self-hosted version of Puter is different in a few ways from [Puter.com](https://puter.com):
- There is no built-in way to access apps from puter.com (see below)
- Several "core" apps are missing, such as **Code** or **Draw**
- Some assets are different
Work is ongoing to improve the **App Center** and make it available on self-hosted.
Until then, it is still possible to add apps using the **Dev Center** app.
<br/>
## Configuration
Running the server will generate a [configuration file](./config.md) in one of these locations:
- `config/config.json` when [Using Docker](#using-docker)
- `volatile/config/config.json` in [Local Development](#local-development)
- `/etc/puter/config.json` on a server (or within a Docker container)
### Domain Name
To access Puter on your device, you can simply go to the address printed in
the server console (usually `puter.localhost:4100`).
To access Puter from another device on LAN, enable the following configuration:
```json
"allow_nipio_domains": true
```
To access Puter from another device, a domain name must be configured, as well as
an `api` subdomain. For example, `example.local` might be the domain name pointing
to the IP address of the server running puter, and `api.example.com` must point to
this address as well. This domain must be specified in the configuration file
(usually `volatile/config/config.json`) as well.
See [domain configuration](./domains.md) for more information.
### Configure the Port
- You can specify a custom port by setting `http_port` to a desired value
- If you're using a reverse-proxy such as nginx or cloudflare, you should
also set `pub_port` to the public (external) port (usually `443`)
- If you have HTTPS enabled on your reverse-proxy, ensure that
`protocol` in config.json is set accordingly
### Default User
By default, Puter will create a user called `default_user`.
This user will have a randomly generated password, which will be printed
in the development console.
A warning will persist in the dev console until this user's
password is changed. Please login to this user and change the password as
your first step.
<br/>
-39
View File
@@ -1,39 +0,0 @@
## Puter Support Levels for Repository Updates
This document describes issues requiring repository changes;
which issues will be fixed by Puter's core team, and which ones
will be fixed if the community makes a contribution.
This document is not "law". It is provided only as a helpful guide
on what to expect.
### Level Glossary
| Name | Description |
| ---- | ----------- |
| Core | Core developers will fix this |
| Community | We will accept contributions to fix this |
| Mixed | Core developers will fix this if it's currently a priority |
### Issues and their Levels
| Issue | Priority |
| ----- | -------- |
| Security vulnerability | Core |
| Breaking change to SDK or API | Core |
| Bug in service in CoreModule | Core |
| Bug in a built-in app | Core |
| Login/init failure in Docker on `release` branch | Core |
| Login/init failure in Linux or OSX | Core |
| Login/init failure in Docker on `main` branch | Mixed |
| Login/init failure with specific configuration | Mixed |
| Login/init failure in Windows | Community |
## Puter Support for a Particular Deployment
If you experience issues on a self-hosted deployment we're here to
help. Some issues are related to configuration or environment, so
we may only be able to help in a limited capacity. Issues related
to data loss, data corruption, or security will have higher priority
over other issues with particular deployments.
+488
View File
@@ -0,0 +1,488 @@
# Self-Hosting Puter
`docker-compose.yml` brings up Puter **plus every external service it needs** — MariaDB, Valkey, DynamoDB-local, RustFS S3, nginx — wired together. Closest thing to a production deployment you can self-manage on a single host.
## One-line installer (recommended)
```bash
curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh
```
Generates secrets, writes `.env` + `puter/config/config.json`, downloads `docker-compose.yml` from the OSS repo, and runs `docker compose up -d`. Re-running is safe — it won't overwrite existing config (set `PUTER_FORCE=1` to rotate). Use the manual steps below if you want to inspect or tweak each step yourself.
## Requirements
- **Docker** with the `compose` plugin.
- A **domain** with DNS access — you need a wildcard record (`*.your-domain.com` → server IP). Puter routes by subdomain (`api.<domain>`, `site.<domain>`, `app.<domain>`).
- Optional: **TLS certs** (or `certbot` to grab them — see Step 3).
## What's running
| Container | Image | Role |
| --------------- | ------------------------ | ---------------------------------------------------------- |
| `puter-nginx` | `nginx:1.27-alpine` | Reverse proxy on 80 (and 443 if TLS); forwards to Puter |
| `puter` | `ghcr.io/heyputer/puter` | The app |
| `puter-mariadb` | `mariadb:11` | SQL database — schema applied automatically on first boot |
| `puter-valkey` | `valkey/valkey:8-alpine` | Redis-compatible cache + rate-limiter |
| `puter-dynamo` | `amazon/dynamodb-local` | KV store — table auto-created on first boot |
| `puter-s3` | `rustfs/rustfs` | S3-compatible object storage (MinIO drop-in noted in file) |
| `puter-s3-init` | `amazon/aws-cli` | One-shot — creates the bucket on first boot, then exits |
Optional services (compose profile `ai`, opt-in):
| Container | Image | Role |
| ------------------- | --------------- | -------------------------------------------------------------- |
| `puter-ollama` | `ollama/ollama` | Local LLM provider (CPU; GPU passthrough opt-in) |
| `puter-ollama-init` | `ollama/ollama` | One-shot — pulls the default model (`tinyllama`) on first boot |
State lives under `./puter/data/<service>/`.
---
## Step 1 — Create `.env` and `puter/config/config.json`
> ⚠️ **Run this whole block in one shell session.** It generates secrets once and writes them into both `.env` (read by docker compose) and `config.json` (read by Puter). The two files **must** agree on the MariaDB password and the S3 secret — if they drift, MariaDB initialises with one password and Puter tries to log in with another, and you get `ER_ACCESS_DENIED_ERROR`.
```bash
MARIADB_ROOT_PASSWORD=$(openssl rand -hex 32)
MARIADB_PASSWORD=$(openssl rand -hex 32)
S3_SECRET_KEY=$(openssl rand -hex 32)
JWT_SECRET=$(openssl rand -hex 64)
JWT_SECRET_V2=$(openssl rand -hex 64)
URL_SIGNATURE_SECRET=$(openssl rand -hex 64)
cat > .env <<EOF
HTTP_PORT=80
# HTTPS_PORT=443 # uncomment after enabling TLS in Step 3
MARIADB_ROOT_PASSWORD=$MARIADB_ROOT_PASSWORD
MARIADB_DATABASE=puter
MARIADB_USER=puter
MARIADB_PASSWORD=$MARIADB_PASSWORD
S3_ACCESS_KEY=puter
S3_SECRET_KEY=$S3_SECRET_KEY
S3_BUCKET=puter-local
EOF
mkdir -p puter/config puter/data puter/tls
cat > puter/config/config.json <<EOF
{
"domain": "puter.localhost",
"protocol": "http",
"pub_port": 80,
"env": "prod",
"static_hosting_domain": "site.puter.localhost",
"static_hosting_domain_alt": "host.puter.localhost",
"private_app_hosting_domain": "app.puter.localhost",
"private_app_hosting_domain_alt": "dev.puter.localhost",
"jwt_secret": "$JWT_SECRET",
"jwt_secret_v2": "$JWT_SECRET_V2",
"allow_v1_tokens": true,
"url_signature_secret": "$URL_SIGNATURE_SECRET",
"database": {
"engine": "mysql",
"host": "mariadb",
"port": 3306,
"user": "puter",
"password": "$MARIADB_PASSWORD",
"database": "puter",
"migrationPaths": ["/opt/puter/dist/src/backend/clients/database/migrations/mysql"]
},
"redis": {
"startupNodes": [{ "host": "valkey", "port": 6379 }],
"tls": false
},
"dynamo": {
"endpoint": "http://dynamo:8000",
"bootstrapTables": true,
"aws": {
"access_key": "fake",
"secret_key": "fake",
"region": "us-east-1"
}
},
"s3": {
"s3Config": {
"endpoint": "http://s3:9000",
"publicEndpoint": "http://s3.puter.localhost",
"accessKeyId": "puter",
"secretAccessKey": "$S3_SECRET_KEY",
"region": "us-east-1",
"forcePathStyle": true
}
},
"s3_bucket": "puter-local",
"s3_region": "us-east-1",
"providers": {
"ollama": { "enabled": false }
},
"trust_proxy": 1
}
EOF
```
Replace `puter.localhost`, `site.puter.localhost`, `host.puter.localhost`, `dev.puter.localhost` and `app.puter.localhost` with your actual domain (or leave it for a localhost-only trial).
Why these knobs:
- `jwt_secret` + `jwt_secret_v2` — Puter signs new auth tokens with `jwt_secret_v2` (v2 token format, `kid: 'v2'` JWT header). `jwt_secret` is verify-only and lets tokens minted before this version (cookies still in users' browsers, stored API tokens) keep working. `allow_v1_tokens: true` keeps that fallback enabled. Fresh installs can leave it as-is; future versions will flip the flag to `false` and retire `jwt_secret` entirely once legacy tokens have drained.
- `env: "prod"` — the bundled `config.default.json` ships with `env: "dev"` (matches the source-tree `npm run start:gui` workflow, which expects webpack-dev-server emitting a CSS manifest). Self-host runs against pre-built static bundles, so `env: "prod"` makes the homepage emit the `/dist/bundle.min.css` `<link>` tag instead of waiting on a manifest that doesn't exist.
- `database.migrationPaths` — Puter applies the bundled MySQL/MariaDB schema on boot. The migration files are idempotent, so it is safe to leave this configured across restarts.
- `dynamo.bootstrapTables: true` — Puter creates its KV table on boot. **Only set against a local emulator**, never real AWS.
- `dynamo.aws` keys are dummies; DynamoDB-local doesn't validate them but the AWS SDK requires _something_. **Note:** DynamoDB uses `access_key` / `secret_key` (snake_case); S3 below uses `accessKeyId` / `secretAccessKey` (camelCase). Not interchangeable.
- `providers.ollama.enabled: false` — Puter auto-probes a local Ollama at `127.0.0.1:11434` by default; without one running you'd see `ECONNREFUSED` on every boot. To run a bundled Ollama, see [Optional: local LLM (Ollama)](#optional-local-llm-ollama) below.
- `s3.s3Config.forcePathStyle: true` — RustFS / MinIO / fauxqs need path-style URLs (`<endpoint>/<bucket>`). Real AWS S3 wants virtual-hosted (`<bucket>.<endpoint>`) — drop this flag (or set `false`) when you swap to real S3.
- `s3.s3Config.publicEndpoint` — `endpoint` (`http://s3:9000`) only resolves inside the docker network; presigned upload/download URLs handed to the browser need a host-reachable URL. nginx routes the `s3.<domain>` subdomain to RustFS internally and preserves the Host header end-to-end (required for S3 signature validation), so the browser hits the same port/protocol as the rest of the app — no separate published port, no mixed-content surprises when you turn on TLS. Switch to `https://s3.<your-domain>` once you enable TLS in Step 3. Real AWS S3 doesn't need this — its endpoint is already public; drop the field entirely.
- `trust_proxy: 1` — nginx terminates TLS and forwards `X-Forwarded-For`. Without this, `req.ip` is the docker-network address of the nginx container instead of the real client IP, which breaks rate limiting and IP-based audit logs. `1` = one trusted hop (nginx). Bump to `2` if you put Cloudflare in front of nginx; never set `true` (it trusts every hop and makes XFF forgeable).
> If you ever change `MARIADB_PASSWORD` after first boot, `.env` alone won't update MariaDB — its credentials are baked into `./puter/data/mariadb/` on first init. Either rotate the password inside MariaDB by hand or `docker compose down && rm -rf ./puter/data/mariadb` to start fresh.
## Step 2 — Point DNS at the server \[Optional\]
In your DNS provider, add records for the main domain plus the subdomains Puter and nginx route on (`api.*`, `site.*`, `app.*`, `s3.*`):
```
A puter.localhost → <your server's public IP>
A *.puter.localhost → <your server's public IP>
A site.puter.localhost → <your server's public IP>
A *.site.puter.localhost → <your server's public IP>
A host.puter.localhost → <your server's public IP>
A *.host.puter.localhost → <your server's public IP>
A app.puter.localhost → <your server's public IP>
A *.app.puter.localhost → <your server's public IP>
A dev.puter.localhost → <your server's public IP>
A *.dev.puter.localhost → <your server's public IP>
```
The wildcards are required — Puter routes via subdomains (`api.*`, `app.*`, etc.) and nginx routes browser S3 traffic via `s3.*` to RustFS.
## Step 3 — TLS (recommended for public installs) \[Optional\]
Skip this for a quick local demo. Don't skip it for users typing passwords.
**Get a wildcard cert.** Easiest path with Let's Encrypt + DNS-01 (works for wildcards):
```bash
sudo certbot certonly --manual --preferred-challenges dns \
-d puter.localhost -d "*.puter.localhost" \
-d site.puter.localhost -d "*.site.puter.localhost" \
-d host.puter.localhost -d "*.host.puter.localhost" \
-d app.puter.localhost -d "*.app.puter.localhost" \
-d dev.puter.localhost -d "*.dev.puter.localhost"
```
The cert needs to cover `*.puter.localhost` so that `s3.puter.localhost` (browser S3 endpoint), plus Puter's own `api.*` / `app.*` subdomains, all validate.
Drop the resulting `fullchain.pem` and `privkey.pem` into `./puter/tls/`.
**Wire nginx to use them:**
1. Open [nginx/nginx.conf](../nginx/nginx.conf), uncomment **both** `# server { listen 443 ssl … }` blocks (one for `s3.*`, one for the catch-all).
2. (Optional) Replace the body of the port-80 blocks with `return 301 https://$host$request_uri;` to force HTTPS everywhere.
3. In [docker-compose.yml](../docker-compose.yml), uncomment the `443:443` port mapping under the `nginx` service.
4. In `.env`, uncomment `HTTPS_PORT=443`.
5. In `config.json`, switch:
```json
{ "protocol": "https", "pub_port": 443 }
```
…and update the S3 public endpoint:
```json
"s3": { "s3Config": { "publicEndpoint": "https://s3.puter.local", ... } }
```
## Running behind your own reverse proxy
The bundled `puter-nginx` mirrors production and is the supported default — it terminates TLS (Step 3) and forwards every Host to Puter. But plenty of self-hosters already run their own edge proxy (Caddy, Traefik, HAProxy, a cloud load balancer, another nginx). You can put Puter behind it instead; Puter doesn't terminate TLS itself in any setup, so "your proxy does TLS" is just a matter of pointing it at the Puter container and getting a handful of details right.
You can keep the bundled nginx as the single hop your proxy talks to, or bypass it and forward straight to the Puter container on port `4100` (uncomment the `4100:4100` mapping under the `puter` service in [docker-compose.yml](../docker-compose.yml), or attach your proxy to the compose network). Either works — what matters is the rules below.
**The rules that actually matter** (getting any of these wrong is what causes the redirect loops and "Invalid Host header" failures people hit):
1. **Don't rewrite the `Host` header.** Puter routes entirely on Host — `api.<domain>`, `site.<domain>`, `app.<domain>` are all distinguished by the incoming host. Forward the original host through unchanged. Do **not** rewrite external hostnames to an internal name like `puter.local`; that forces you to remap every subdomain by hand and still breaks signed-URL and CORS checks. Most proxies preserve Host by default — just don't override it.
2. **The external domain must equal `domain` in your `config.json`.** If users reach the box at `puter.example.com`, then `domain` must be `puter.example.com` and the hosting domains must be its real subdomains (`site.puter.example.com`, `app.puter.example.com`, …) — exactly as the installer/`config.json` lays them out. Puter rejects hosts it doesn't recognize with `Invalid Host header`.
3. **Set `protocol` to match the public scheme.** When your proxy terminates TLS, set `"protocol": "https"` (installer: `PUTER_PROTOCOL=https`). Puter builds its origins, redirects, signed S3 URLs, and OIDC callback URLs from this — leave it `http` behind an HTTPS proxy and you get redirect loops, broken logins, and mixed-content errors.
4. **Forward the standard proxy headers.** Puter needs:
- `Host` — the original request host (rule 1).
- `X-Forwarded-Proto` — the **external** scheme (`https`), so Puter knows TLS was terminated upstream.
- `X-Forwarded-For` — the real client IP (rate limiting + audit logs).
- `Upgrade` / `Connection` — passed through for WebSocket / socket.io upgrades, or the realtime connection silently fails.
5. **Set `trust_proxy` to the number of hops in front of Puter.** One external proxy talking directly to Puter → `"trust_proxy": 1` (installer: `PUTER_TRUST_PROXY=1`). A second proxy in front (e.g. Cloudflare → your proxy → Puter) → `2`. This is the count of proxies between the client and Puter, **including** the bundled nginx if you keep it. Too low and `req.ip` becomes a proxy address (breaks rate limiting); never set `true` (it trusts every hop and makes `X-Forwarded-For` forgeable).
6. **Route the wildcard to Puter.** Your proxy must forward `*.<domain>` (covering `api.*`, `app.*`, and `s3.<domain>`) and `*.site.<domain>` (and any other hosting domains) to Puter — same wildcard DNS as Step 2. Puter does the per-subdomain routing internally; the proxy just needs to hand it the traffic with the Host intact.
With those in place the rest of the stack is unchanged — `docker compose up -d` as below.
## Step 4 — Bring it up
```bash
docker compose up -d
```
First boot takes ~30s while MariaDB initialises and Puter applies the schema + default apps. Watch:
```bash
docker compose logs -f puter
```
Healthy startup:
```
[config] override from /etc/puter/config.json
[mysql] running migrations from /opt/puter/dist/src/backend/clients/database/migrations/mysql: 2 file(s)
[mysql] applied mysql_mig_1.sql (...)
[mysql] applied mysql_mig_2.sql (9 statements)
```
Then open **<https://puter.local>** (or `http://` if you skipped TLS). Login is `admin` — the temp password is printed once in the puter container logs on first boot:
```bash
docker compose logs puter | grep tmp_password
```
Change it in Settings after first login.
## Additional configuration
All optional. Drop any of the blocks below into `puter/config/config.json` and `docker compose restart puter`. See [config.template.jsonc](../config.template.jsonc) for the full list. Per-key documentation lives in [src/backend/types.ts](../src/backend/types.ts).
### PostgreSQL database
> **Community contribution — expect rough edges.** PostgreSQL support was contributed by the community and is not run by Puter.com production. It boots, applies the bundled schema, and exercises the common user/app/fsentry/session/permission/OIDC flows in the integration tests, but less-traveled SQL call sites may still need porting. If you hit a query that doesn't work on Postgres, please open an issue. For production self-hosting today, MariaDB/MySQL and SQLite are the supported defaults.
The bundled Docker Compose stack still defaults to MariaDB. To use PostgreSQL instead, run a PostgreSQL service yourself, point Puter at it, and use the PostgreSQL migration path:
```json
"database": {
"engine": "postgres",
"host": "postgres",
"port": 5432,
"user": "puter",
"password": "...",
"database": "puter",
"migrationPaths": ["/opt/puter/dist/src/backend/clients/database/migrations/postgres"]
}
```
You may use `"connectionString": "postgres://puter:...@postgres:5432/puter"` instead of the host/user/password fields. Puter only needs normal PostgreSQL connection details and the migration path; CloudNativePG is one possible Kubernetes operator, but no operator-specific manifests are required by Puter.
### Email (SMTP)
Used for password resets, email confirmation, and notifications. Without it those flows silently fail.
```json
"email": {
"from": "\"Puter\" <[email protected]>",
"host": "smtp.example.com",
"port": 587,
"secure": false,
"auth": { "user": "...", "pass": "..." }
}
```
To require email confirmation before login, also set `"strict_email_verification_required": true`.
### Sign in with Google (or another OIDC provider)
```json
"oidc": {
"providers": {
"google": {
"client_id": "...apps.googleusercontent.com",
"client_secret": "...",
"scopes": "openid email profile"
}
}
}
```
Add `https://puter.<your-domain>/auth/oidc/callback/login` to the OAuth client's authorized redirect URIs in the Google Cloud Console. For non-Google providers, replace `google` with a custom id and supply `authorization_endpoint` / `token_endpoint` / `userinfo_endpoint` explicitly.
### Sign in with Apple
```json
"oidc": {
"providers": {
"apple": {
"client_id": "com.example.your-service-id",
"team_id": "YOUR_TEAM_ID",
"key_id": "YOUR_KEY_ID",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
}
}
}
```
The `client_id` is your Apple Services ID. `team_id`, `key_id`, and `private_key` come from the Apple Developer Portal (Keys section — create a key with "Sign in with Apple" enabled). The `private_key` is the contents of the `.p8` file Apple provides. Add `https://puter.<your-domain>/auth/oidc/callback/login` and `https://puter.<your-domain>/auth/oidc/callback/signup` as return URLs in the Apple Services ID configuration.
### Sign in with Microsoft
```json
"oidc": {
"providers": {
"microsoft": {
"client_id": "YOUR_APPLICATION_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET_VALUE",
"tenant_id": "YOUR_TENANT_ID"
}
}
}
```
Register an app in the Azure Portal (Microsoft Entra ID → App registrations). The `client_id` is the Application (client) ID, `client_secret` is a client secret value, and `tenant_id` is the Directory (tenant) ID. Use `common` as `tenant_id` to allow any Microsoft account (personal and organizational); omit it to default to `common`. Add `https://puter.<your-domain>/auth/oidc/callback/login` and `https://puter.<your-domain>/auth/oidc/callback/signup` as redirect URIs under Authentication in the app registration.
### AI providers
Any provider with a key set is auto-enabled. Same shape as `ollama` above:
```json
"providers": {
"claude": { "apiKey": "sk-ant-..." },
"openai-completion": { "apiKey": "sk-..." },
"gemini": { "apiKey": "..." },
"openai-image-generation": { "apiKey": "sk-..." }
}
```
Full provider list (chat, image, video, TTS, OCR) is in the template.
### Per-user storage quota
Default is 100 MB per user.
```json
"storage_capacity": 5368709120, // 5 GB
"is_storage_limited": true
```
Set `is_storage_limited: false` for unlimited (bounded by host disk).
### Captcha on signup / login
Built-in proof-of-work captcha — no external service needed.
```json
"captcha": { "enabled": true, "difficulty": "medium" }
```
`difficulty` is one of `easy` / `medium` / `hard`.
### Disable new signups
Force visitors to log in with an existing account instead of creating a
temporary or permanent one.
```json
"disable_user_signup": true
```
### Block disposable email TLDs
Only enforced when `env: "prod"`.
```json
"blockedEmailDomains": ["mailinator.com", "tempmail.com", "guerrillamail.com"]
```
### Password policy
```json
"min_pass_length": 12
```
### Contact-form recipient
Where the in-app contact form posts. Defaults to `support@puter.com`.
```json
"support_email": "[email protected]"
```
## Optional: local LLM (Ollama)
The `ollama` and `ollama-init` services live behind a compose profile so they don't run unless you ask for them. By default, `puter/config/config.json` has `"ollama": { "enabled": false }` — Puter skips the auto-probe entirely. To run a local model:
1. Flip the config:
```json
"providers": {
"ollama": { "apiBaseUrl": "http://ollama:11434" }
}
```
2. (Optional) Pick a model in `.env`:
```bash
OLLAMA_DEFAULT_MODEL=tinyllama # default — 1.1B, ~640 MB on disk, ~700 MB RAM
# Other tiny picks: qwen2.5:0.5b, llama3.2:1b
# Larger / better: phi3.5, llama3.2, mistral
```
3. Bring up with the `ai` profile:
```bash
docker compose --profile ai up -d
docker compose logs -f ollama-init
```
`ollama-init` exits 0 once the model is pulled. Subsequent boots find the model already on disk and the pull is a fast no-op.
Without `--profile ai`, the `ollama` containers stay down and Puter (with `enabled: false`) doesn't try to reach them — the rest of the stack runs identically.
For GPU acceleration (NVIDIA), uncomment the `deploy:` block under the `ollama` service in [docker-compose.yml](../docker-compose.yml). Requires `nvidia-container-toolkit` on the host.
## Building from source instead of pulling
If you want to test local Dockerfile changes against the full stack, uncomment the `build:` block in [docker-compose.yml](../docker-compose.yml) under the `puter` service, change `pull_policy: always` → `pull_policy: never`, then:
```bash
docker compose up -d --build
```
---
## Managing running backend
```bash
# update
docker compose pull
docker compose up -d
# logs
docker compose logs -f puter
# stop, keep data
docker compose down
# stop, NUKE all state (irreversible)
docker compose down
rm -rf puter/data
```
Migrations re-apply idempotently across pulls. Volumes are preserved.
## Troubleshooting
**Site loads but I get "Bad Gateway" / nginx errors.**
The puter container failed to come up. `docker compose logs puter` will tell you which dependency rejected it (most often DB password mismatch between `.env` and `config.json`).
**Login screen says "admin password not set".**
First-boot temp password is logged once. Find it: `docker compose logs puter | grep "tmp_password"`. After login, change it in Settings.
**Healthcheck reports unhealthy but the site works.**
The healthcheck hits `puter.localhost:4100/test` from inside the container. If you changed `domain` or `port`, the check still uses defaults. The site itself is fine.
**Nothing resolves at `puter.example.com` after DNS changes.**
DNS propagates slowly. `dig puter.example.com` and `dig api.puter.example.com` should both return your server IP. If not, give it 5–60 minutes.
**`docker compose up` hangs at "waiting for service to be healthy".**
`docker compose ps` shows which container is unhealthy. MariaDB takes ~20–30s on a cold boot; everything else under 5s. If something stays unhealthy, `logs <service>` will tell you why.
**`Error: DynamoDB aws config requires both access_key and secret_key`.**
You wrote `accessKeyId` / `secretAccessKey` under `dynamo.aws`. That config block uses snake_case (`access_key` / `secret_key`). Only the `s3.s3Config` block uses camelCase.
-31
View File
@@ -1,31 +0,0 @@
# Testing with Email
Testing anything involving email is really simple using [mailhog](https://github.com/mailhog/MailHog)
### Step 1: Configure email service
In your `config.json` for Puter (`volatile/config/config.json` usually, `/var/puter/config.json` in containers),
add this entry to the `"services`" map:
```javascript
"services": {
// ... there are probably other service configs
"email": {
"host": "localhost",
"port": 1025
}
}
```
### Step 2: Install and run mailhog
Follow the instructions on [MailHog](https://github.com/mailhog/MailHog)'s
repository, or install through your distro's package manager.
Run the command: `mailhog`.
You should now have an inbox at [http://127.0.0.1:8025](http://127.0.0.1:8025).
Every email that Puter sends will show up on this page.
+281 -13
View File
@@ -1,25 +1,293 @@
---
version: "3.8"
# Self-hosted Puter — full stack.
#
# Brings up Puter + every external service it needs:
# - nginx : reverse proxy (mirrors prod ALB; handles TLS + Host fan-out)
# - valkey : redis-compatible cache / rate-limiter backend
# - mariadb : SQL database (Puter applies its schema on first boot)
# - dynamo : DynamoDB-local (KV store; Puter creates the table itself)
# - s3 : RustFS — S3-compatible object storage
# - s3-init : one-shot init container that creates the bucket
# - puter : the application
#
# Quick start:
# 1. Copy .env.example to .env (or set the variables in your shell).
# 2. Drop a config.json into ./puter/config/ — see selfhosted/full-stack.md
# for the example that pairs with this compose.
# 3. docker compose up -d
#
# Easiest path:
# curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh
# grabs this file, generates secrets, writes .env + config.json, and runs
# the compose up for you.
#
# Production:
# - Always replace the default passwords / S3 keys / Puter secrets.
# - Front Puter with TLS-terminating reverse proxy (Caddy / nginx).
# - Move state-bearing volumes to a backed-up location.
services:
puter:
container_name: puter
image: ghcr.io/heyputer/puter:latest
pull_policy: always
# build: ./
valkey:
image: valkey/valkey:8-alpine
container_name: puter-valkey
restart: unless-stopped
# Run as a single-node cluster so Puter's ioredis Cluster client
# (the only mode it speaks) can connect. On first boot we assign all
# 16384 slots to ourselves; subsequent boots find them already in
# nodes.conf and skip. `cluster-require-full-coverage no` keeps reads
# working if we ever land partial slots.
command:
- sh
- -c
- |
valkey-server \
--port 6379 \
--cluster-enabled yes \
--cluster-config-file /data/nodes.conf \
--cluster-node-timeout 5000 \
--cluster-require-full-coverage no \
--cluster-announce-ip valkey \
--cluster-announce-port 6379 \
--cluster-announce-bus-port 16379 \
--appendonly yes \
--save "60 1" &
SERVER_PID=$$!
until valkey-cli -p 6379 PING > /dev/null 2>&1; do sleep 0.5; done
if ! valkey-cli -p 6379 CLUSTER NODES | grep -q '0-16383'; then
valkey-cli -p 6379 CLUSTER ADDSLOTSRANGE 0 16383
fi
wait $$SERVER_PID
volumes:
# `:z` is an SELinux relabel hint for Fedora/RHEL hosts (no-op
# everywhere else) — without it those distros deny container
# access to the bind mount and the service loops on EACCES.
- ./puter/data/valkey:/data:z
healthcheck:
test:
["CMD-SHELL", "valkey-cli -p 6379 cluster info | grep -q cluster_state:ok"]
interval: 5s
timeout: 3s
retries: 20
start_period: 10s
mariadb:
image: mariadb:11
container_name: puter-mariadb
restart: unless-stopped
ports:
- '4100:4100'
environment:
# TZ: Europe/Paris
# CONFIG_PATH: /etc/puter
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-root-change-me}
MARIADB_DATABASE: ${MARIADB_DATABASE:-puter}
MARIADB_USER: ${MARIADB_USER:-puter}
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-puter-change-me}
volumes:
- ./puter/data/mariadb:/var/lib/mysql:z
healthcheck:
# `healthcheck.sh` ships with the mariadb image; --connect verifies
# the server is accepting auth, not just listening on the socket.
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 5s
retries: 20
start_period: 30s
dynamo:
# Puter creates the `store-kv-v1` table itself on startup
# (config.dynamo.bootstrapTables = true does the work).
image: amazon/dynamodb-local:latest
container_name: puter-dynamo
restart: unless-stopped
user: "1000:1000"
working_dir: /home/dynamodblocal
command:
- "-jar"
- "DynamoDBLocal.jar"
- "-sharedDb"
- "-dbPath"
- "/home/dynamodblocal/data"
volumes:
- ./puter/data/dynamo:/home/dynamodblocal/data:z
s3:
# RustFS — S3-compatible object storage. Drop-in alternative:
# MinIO (image: minio/minio, command: ["server", "/data", "--console-address", ":9001"]).
image: rustfs/rustfs:latest
container_name: puter-s3
restart: unless-stopped
environment:
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:-puter}
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-puter-secret-change-me}
volumes:
- ./puter/data/s3:/data:z
# Internal-only — browsers reach RustFS via nginx (`s3.<domain>`),
# which preserves the Host header for S3 signature validation and
# rides the same TLS termination as Puter. Uncomment to also expose
# 9000 directly on the host for `aws-cli` / debugging.
# ports:
# - "9000:9000"
healthcheck:
# RustFS exposes /health on the S3 port. Use wget (curl is not in
# the slim image).
test:
[
"CMD-SHELL",
"wget -qO- --tries=1 --timeout=2 http://localhost:9000/health || exit 1",
]
interval: 5s
timeout: 3s
retries: 20
start_period: 5s
s3-init:
# One-shot container that creates the `puter-local` bucket on first
# boot. Exits 0 once the bucket exists; stays exited 0 thereafter.
image: amazon/aws-cli:latest
container_name: puter-s3-init
depends_on:
s3:
condition: service_healthy
environment:
AWS_ACCESS_KEY_ID: ${S3_ACCESS_KEY:-puter}
AWS_SECRET_ACCESS_KEY: ${S3_SECRET_KEY:-puter-secret-change-me}
AWS_DEFAULT_REGION: us-east-1
entrypoint:
- /bin/sh
- -c
- |
set -e
endpoint=http://s3:9000
bucket=${S3_BUCKET:-puter-local}
if aws --endpoint-url "$$endpoint" s3api head-bucket --bucket "$$bucket" 2>/dev/null; then
echo "bucket $$bucket already exists"
else
echo "creating bucket $$bucket"
aws --endpoint-url "$$endpoint" s3 mb "s3://$$bucket"
fi
restart: "no"
# ── Optional: local LLM ───────────────────────────────────────────
# Behind the `ai` compose profile — only starts when explicitly opted
# into. Bring up with:
# docker compose --profile ai up -d
# When enabled, also set in your `puter/config/config.json`:
# "providers": { "ollama": { "apiBaseUrl": "http://ollama:11434" } }
# When NOT enabled, set:
# "providers": { "ollama": { "enabled": false } }
# otherwise Puter spams `ECONNREFUSED 127.0.0.1:11434` on startup.
ollama:
profiles: ["ai"]
# CPU-only out of the box; uncomment the GPU `deploy:` block below
# if you've got nvidia-docker for much faster inference. Disk + RAM
# scale with the model — `tinyllama` (1.1B, ~640 MB on disk, ~700
# MB RAM) is the cheapest sane default. Swap via OLLAMA_DEFAULT_MODEL.
image: ollama/ollama:latest
container_name: puter-ollama
restart: unless-stopped
volumes:
- ./puter/data/ollama:/root/.ollama:z
# Uncomment to expose Ollama directly on the host (`localhost:11434`)
# for `ollama` CLI / OpenAI-API compatible tools. Internal-only by default.
# ports:
# - "11434:11434"
healthcheck:
test:
["CMD-SHELL", "ollama list >/dev/null 2>&1 || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
# GPU passthrough (NVIDIA). Requires nvidia-container-toolkit on host.
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: all
# capabilities: [gpu]
ollama-init:
profiles: ["ai"]
# One-shot — ensures the default model is present. `ollama pull` is
# idempotent: present-and-up-to-date → fast no-op; missing → downloads.
image: ollama/ollama:latest
container_name: puter-ollama-init
depends_on:
ollama:
condition: service_healthy
environment:
OLLAMA_HOST: http://ollama:11434
OLLAMA_DEFAULT_MODEL: ${OLLAMA_DEFAULT_MODEL:-tinyllama}
entrypoint:
- /bin/sh
- -c
- |
set -e
echo "[ollama-init] ensuring $${OLLAMA_DEFAULT_MODEL}"
ollama pull "$${OLLAMA_DEFAULT_MODEL}"
echo "[ollama-init] done"
restart: "no"
puter:
image: ghcr.io/heyputer/puter:main
pull_policy: always
# Uncomment to build from this directory instead of pulling the published
# image. Also flip pull_policy to `never` so compose doesn't overwrite
# your local build by re-pulling :latest.
# build:
# context: .
# # buildx-only: cross-compile to both archs in a single push
# # platforms:
# # - linux/amd64
# # - linux/arm64
container_name: puter
restart: unless-stopped
depends_on:
valkey:
condition: service_healthy
mariadb:
condition: service_healthy
dynamo:
condition: service_started
s3-init:
condition: service_completed_successfully
# Internal-only: nginx reaches it on the compose network. Uncomment
# to also expose port 4100 directly on the host (useful for debugging).
# ports:
# - "4100:4100"
expose:
- "4100"
environment:
PUID: 1000
PGID: 1000
volumes:
- ./puter/config:/etc/puter
- ./puter/data:/var/puter
# Drop your config.json here — see selfhosted/full-stack.md.
- ./puter/config:/etc/puter:z
# Persistent runtime data (anything your config points at /var/puter).
- ./puter/data/puter:/var/puter:z
healthcheck:
test: wget --no-verbose --tries=1 --spider http://puter.localhost:4100/test || exit 1
test: wget --no-verbose --tries=1 --spider http://localhost:4100/ || exit 1
interval: 30s
timeout: 3s
retries: 3
start_period: 30s
nginx:
image: nginx:1.27-alpine
container_name: puter-nginx
restart: unless-stopped
depends_on:
puter:
condition: service_started
ports:
- "${HTTP_PORT:-80}:80"
# Uncomment when you enable TLS in nginx/nginx.conf:
# - "${HTTPS_PORT:-443}:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro,z
# TLS certs (fullchain.pem + privkey.pem). Read-only inside.
- ./puter/tls:/etc/nginx/tls:ro,z
healthcheck:
test: ["CMD-SHELL", "wget -qO- --tries=1 --timeout=2 http://localhost/ || exit 1"]
interval: 10s
timeout: 3s
retries: 5
start_period: 5s
+96 -170
View File
@@ -1,204 +1,130 @@
import js from '@eslint/js';
import stylistic from '@stylistic/eslint-plugin';
import tseslintPlugin from '@typescript-eslint/eslint-plugin';
import tseslintParser from '@typescript-eslint/parser';
import prettierPlugin from 'eslint-plugin-prettier';
import prettierConfig from 'eslint-config-prettier';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import bangSpaceIf from './eslint/bang-space-if.js';
import controlStructureSpacing from './eslint/control-structure-spacing.js';
import spaceUnaryOpsWithException from './eslint/space-unary-ops-with-exception.js';
export const rules = {
'no-invalid-this': 'error',
'no-unused-vars': ['error', {
vars: 'all',
args: 'after-used',
caughtErrors: 'none',
ignoreRestSiblings: false,
ignoreUsingDeclarations: false,
reportUsedIgnorePattern: false,
argsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
// typescript-eslint's flat/recommended preset is an array of configs (base +
// eslint-recommended overrides + recommended rules). Flatten its rules so we
// can apply them via our own `files`-scoped blocks.
const tsRecommendedRules = tseslintPlugin.configs['flat/recommended'].reduce(
(acc, cfg) => ({ ...acc, ...(cfg.rules ?? {}) }),
{},
);
}],
curly: ['error', 'multi-line'],
'@stylistic/curly-newline': ['error', 'always'],
'@stylistic/object-curly-spacing': ['error', 'always'],
'@stylistic/indent': ['error', 4, {
SwitchCase: 1,
CallExpression: {
arguments: 1,
},
}],
'@stylistic/indent-binary-ops': ['error', 4],
'@stylistic/array-bracket-newline': ['error', 'consistent'],
'@stylistic/semi': ['error', 'always'],
'@stylistic/quotes': ['error', 'single', { 'avoidEscape': true }],
'@stylistic/function-call-argument-newline': ['error', 'consistent'],
'@stylistic/function-paren-newline': ['error', 'multiline-arguments'],
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
'@stylistic/space-before-function-paren': 'error',
'@stylistic/key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }],
'@stylistic/keyword-spacing': ['error', { 'before': true, 'after': true }],
'@stylistic/no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
'@stylistic/comma-spacing': ['error', { 'before': false, 'after': true }],
'@stylistic/comma-dangle': ['error', 'always-multiline'],
'@stylistic/object-property-newline': ['error', { allowAllPropertiesOnSameLine: true }],
'@stylistic/dot-location': ['error', 'property'],
'@stylistic/space-infix-ops': ['error'],
'no-undef': 'error',
'custom/control-structure-spacing': 'error',
'custom/bang-space-if': 'error',
'@stylistic/no-trailing-spaces': 'error',
'@stylistic/space-before-blocks': ['error', 'always'],
'prefer-template': 'error',
'@stylistic/no-mixed-spaces-and-tabs': ['error', 'smart-tabs'],
'custom/space-unary-ops-with-exception': ['error', { words: true, nonwords: false }],
'@stylistic/no-multi-spaces': ['error', { exceptions: { 'VariableDeclarator': true } }],
'@stylistic/type-annotation-spacing': 'error',
'@stylistic/type-generic-spacing': 'error',
'@stylistic/type-named-tuple-spacing': ['error'],
'no-use-before-define': ['error', {
'functions': false,
}],
'@stylistic/array-bracket-spacing': ['error', 'never'],
'@stylistic/linebreak-style': ['error', 'unix'],
'no-useless-computed-key': 'error',
'no-sequences': [
'error', {
allowInParentheses: false,
},
],
const prettierRules = {
...prettierConfig.rules,
'prettier/prettier': 'error',
};
const tsRules = {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', caughtErrors: 'none' }],
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
const unusedVarsOptions = {
args: 'after-used',
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
ignoreRestSiblings: true,
};
const sharedPlugins = {
js,
'@stylistic': stylistic,
custom: {
rules: {
'control-structure-spacing': controlStructureSpacing,
'bang-space-if': bangSpaceIf,
'space-unary-ops-with-exception': spaceUnaryOpsWithException,
},
},
const preferConstOptions = {
destructuring: 'all',
ignoreReadBeforeAssign: false,
};
const sharedJsConfig = {
rules,
plugins: sharedPlugins,
const lintedGlobals = {
...globals.node,
extension: 'readonly',
config: 'readonly',
global_config: 'readonly',
};
const recommendedJsConfig = {
...sharedJsConfig,
extends: ['js/recommended'],
};
const jsFiles = [
'src/backend/**/*.{js,mjs,cjs}',
'extensions/**/*.{js,mjs,cjs}',
];
const createTsConfig = ({ files, project, ignores = [], globals: tsGlobals }) => ({
const tsIgnores = [
'**/*.test.ts',
'**/*.test.mts',
'**/*.spec.ts',
'**/*.spec.mts',
'src/backend/test/**',
'src/backend/tools/**',
'src/backend/vitest.config.ts',
'src/backend/vitest.bench.config.ts',
];
const createTsConfig = ({ files, project }) => ({
files,
ignores,
ignores: tsIgnores,
languageOptions: {
parser: tseslintParser,
...(tsGlobals ? { globals: tsGlobals } : {}),
globals: lintedGlobals,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project,
projectService: { defaultProject: project },
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
'@typescript-eslint': tseslintPlugin,
prettier: prettierPlugin,
},
rules: {
...tsRecommendedRules,
...prettierRules,
'@typescript-eslint/no-unused-vars': ['error', unusedVarsOptions],
'@typescript-eslint/no-explicit-any': 'warn',
'prefer-const': ['error', preferConstOptions],
},
rules: tsRules,
});
const backendConfig = {
...recommendedJsConfig,
files: [
'src/backend/**/*.{js,mjs,cjs,ts}',
'src/putility/**/*.{js,mjs,cjs,ts}',
],
ignores: [
'**/*.test.js',
'**/*.test.ts',
'**/*.test.mts',
],
languageOptions: { globals: globals.node },
};
const testConfig = {
...sharedJsConfig,
files: [
'**/*.test.js',
'**/*.test.ts',
'**/*.test.mts',
],
languageOptions: { globals: { ...globals.node, ...globals.vitest } },
};
const extensionConfig = {
...recommendedJsConfig,
files: ['extensions/**/*.{js,mjs,cjs,ts}'],
languageOptions: {
globals: {
extension: 'readonly',
config: 'readonly',
global_config: 'readonly',
...globals.node,
},
},
};
const frontendConfig = {
...recommendedJsConfig,
files: ['**/*.{js,mjs,cjs,ts}', 'src/gui/src/**/*.js'],
ignores: [
'src/backend/**/*.{js,mjs,cjs,ts}',
'extensions/**/*.{js,mjs,cjs,ts}',
'submodules/**',
'**/*.test.{js,ts,mts,mjs}',
'**/*.min.js',
'**/*.min.cjs',
'**/*.min.mjs',
'**/socket.io.js',
'**/dist/*.js',
'src/gui/src/lib/**',
'src/gui/dist/**',
],
languageOptions: {
globals: {
...globals.browser,
...globals.jquery,
i18n: 'readonly',
puter: 'readonly',
},
},
};
export default defineConfig([
{
ignores: [
'**/*.dbmig.js',
'dist/**',
'build/**',
'volatile/**',
'node_modules/**',
'puter.js/**',
'apps/**',
'experiment/**',
'workers/**',
'src/public/**',
'src/gui/**',
'src/docs/**',
'src/puter-js/**',
'src/worker/**',
'submodules/**',
'tests/**',
'tools/**',
],
},
{
files: jsFiles,
ignores: ['**/*.test.js'],
plugins: {
js,
prettier: prettierPlugin,
},
extends: ['js/recommended'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: lintedGlobals,
},
rules: {
...prettierRules,
'no-unused-vars': ['error', unusedVarsOptions],
'prefer-const': ['error', preferConstOptions],
},
},
createTsConfig({
files: ['**/*.test.ts', '**/*.test.mts', '**/*.test.setup.ts'],
ignores: ['tests/playwright/tests/**/*.ts'],
project: './tests/tsconfig.json',
globals: { ...globals.node, ...globals.vitest },
}),
createTsConfig({
files: ['**/*.ts'],
ignores: ['**/*.test.ts', '**/*.test.mts', 'extensions/**/*.ts'],
files: ['src/backend/**/*.ts', 'extensions/**/*.ts'],
project: './tsconfig.json',
}),
createTsConfig({
files: ['extensions/**/*.ts'],
project: './extensions/tsconfig.json',
}),
backendConfig,
testConfig,
extensionConfig,
frontendConfig,
]);
-69
View File
@@ -1,69 +0,0 @@
// eslint-plugin-bang-space-if/index.js
'use strict';
/** @type {import('eslint').ESLint.Plugin} */
export default {
meta: {
type: 'layout',
docs: {
description:
"Require a space after a top-level '!' in an if(...) condition (e.g., `if ( ! entry )`).",
recommended: false,
},
fixable: 'whitespace',
schema: [], // no options
},
create (context) {
const source = context.getSourceCode();
// Unwrap ParenthesizedExpression layers, if any
function unwrapParens (node) {
let n = node;
// ESLint/ESTree: ParenthesizedExpression is supported by espree
while ( n && n.type === 'ParenthesizedExpression' ) {
n = n.expression;
}
return n;
}
return {
IfStatement (ifNode) {
const testRaw = ifNode.test;
if ( ! testRaw ) return;
const test = unwrapParens(testRaw);
if ( !test || test.type !== 'UnaryExpression' || test.operator !== '!' ) {
return; // only top-level `!` expressions
}
// Ignore boolean-cast `!!x` cases to avoid producing `! !x`
if ( test.argument && test.argument.type === 'UnaryExpression' && test.argument.operator === '!' ) {
return;
}
// Grab operator and argument tokens
const opToken = source.getFirstToken(test); // should be '!'
const argToken = source.getTokenAfter(opToken, { includeComments: false });
if ( !opToken || !argToken ) return;
// Compute current whitespace between '!' and the argument
const between = source.text.slice(opToken.range[1], argToken.range[0]);
// We want exactly one space
if ( between === ' ' ) return;
context.report({
node: test,
loc: {
start: opToken.loc.end,
end: argToken.loc.start,
},
message: "Expected a single space after top-level '!' in if(...) condition.",
fix (fixer) {
return fixer.replaceTextRange([opToken.range[1], argToken.range[0]], ' ');
},
});
},
};
},
};;;;
-206
View File
@@ -1,206 +0,0 @@
export default {
meta: {
type: 'layout',
docs: {
description: 'enforce spacing inside parentheses for control structures only',
category: 'Stylistic Issues',
},
fixable: 'whitespace',
schema: [],
messages: {
missingSpaceAfterOpen: 'Missing space after opening parenthesis in control structure.',
missingSpaceBeforeClose: 'Missing space before closing parenthesis in control structure.',
unexpectedSpaceAfterOpen: 'Unexpected space after opening parenthesis in function call.',
unexpectedSpaceBeforeClose: 'Unexpected space before closing parenthesis in function call.',
},
},
create (context) {
const sourceCode = context.getSourceCode();
function checkControlStructureSpacing (node) {
// For control structures, we need to find the parentheses around the condition/test
let conditionNode;
if ( node.type === 'IfStatement' || node.type === 'WhileStatement' || node.type === 'DoWhileStatement' ) {
conditionNode = node.test;
} else if ( node.type === 'ForStatement' || node.type === 'ForInStatement' || node.type === 'ForOfStatement' ) {
// For loops, we want the parentheses around the entire for clause
conditionNode = node;
} else if ( node.type === 'SwitchStatement' ) {
conditionNode = node.discriminant;
} else if ( node.type === 'CatchClause' ) {
conditionNode = node.param;
}
if ( ! conditionNode ) return;
// Find the opening paren - it should be right before the condition starts
const openParen = sourceCode.getTokenBefore(conditionNode, token => token.value === '(');
if ( !openParen || openParen.value !== '(' ) return;
// Find the closing paren - it should be right after the condition ends
const closeParen = sourceCode.getTokenAfter(conditionNode, token => token.value === ')');
if ( !closeParen || closeParen.value !== ')' ) return;
const afterOpen = sourceCode.getTokenAfter(openParen);
const beforeClose = sourceCode.getTokenBefore(closeParen);
{
const contentBetweenParens = sourceCode.getText().slice(openParen.range[1], closeParen.range[0]);
const isSingleCharVariable = /^\s*[a-zA-Z_$]\s*$/.test(contentBetweenParens);
// Skip spacing requirements for single character variables
if ( isSingleCharVariable ) {
return;
}
}
// Control structures should have spacing
if ( afterOpen && openParen.range[1] === afterOpen.range[0] ) {
context.report({
node,
loc: openParen.loc,
messageId: 'missingSpaceAfterOpen',
fix (fixer) {
return fixer.insertTextAfter(openParen, ' ');
},
});
}
if ( beforeClose && beforeClose.range[1] === closeParen.range[0] ) {
context.report({
node,
loc: closeParen.loc,
messageId: 'missingSpaceBeforeClose',
fix (fixer) {
return fixer.insertTextBefore(closeParen, ' ');
},
});
}
}
function checkForLoopSpacing (node) {
// For loops are special - we need to find the opening paren after the 'for' keyword
// and the closing paren before the body
const forKeyword = sourceCode.getFirstToken(node);
if ( !forKeyword || forKeyword.value !== 'for' ) return;
const openParen = sourceCode.getTokenAfter(forKeyword, token => token.value === '(');
if ( ! openParen ) return;
// The closing paren should be right before the body
const closeParen = sourceCode.getTokenBefore(node.body, token => token.value === ')');
if ( ! closeParen ) return;
const afterOpen = sourceCode.getTokenAfter(openParen);
const beforeClose = sourceCode.getTokenBefore(closeParen);
if ( afterOpen && openParen.range[1] === afterOpen.range[0] ) {
context.report({
node,
loc: openParen.loc,
messageId: 'missingSpaceAfterOpen',
fix (fixer) {
return fixer.insertTextAfter(openParen, ' ');
},
});
}
if ( beforeClose && beforeClose.range[1] === closeParen.range[0] ) {
context.report({
node,
loc: closeParen.loc,
messageId: 'missingSpaceBeforeClose',
fix (fixer) {
return fixer.insertTextBefore(closeParen, ' ');
},
});
}
}
function checkFunctionCallSpacing (node) {
// Find the opening parenthesis for this function call
const openParen = sourceCode.getFirstToken(node, token => token.value === '(');
const closeParen = sourceCode.getLastToken(node, token => token.value === ')');
if ( !openParen || !closeParen ) return;
// Defer multi-line call/new formatting to stylistic paren/argument rules.
if ( openParen.loc.start.line !== closeParen.loc.end.line ) return;
const afterOpen = sourceCode.getTokenAfter(openParen);
const beforeClose = sourceCode.getTokenBefore(closeParen);
// Function calls should NOT have spacing on the same line (multi-line calls are allowed)
if ( afterOpen && openParen.range[1] !== afterOpen.range[0] ) {
const spaceAfter = sourceCode.getText().slice(openParen.range[1], afterOpen.range[0]);
if ( /^\s+$/.test(spaceAfter) && !spaceAfter.includes('\n') ) {
context.report({
node,
loc: openParen.loc,
messageId: 'unexpectedSpaceAfterOpen',
fix (fixer) {
return fixer.removeRange([openParen.range[1], afterOpen.range[0]]);
},
});
}
}
if ( beforeClose && beforeClose.range[1] !== closeParen.range[0] ) {
const spaceBefore = sourceCode.getText().slice(beforeClose.range[1], closeParen.range[0]);
if ( /^\s+$/.test(spaceBefore) && !spaceBefore.includes('\n') ) {
context.report({
node,
loc: closeParen.loc,
messageId: 'unexpectedSpaceBeforeClose',
fix (fixer) {
return fixer.removeRange([beforeClose.range[1], closeParen.range[0]]);
},
});
}
}
}
return {
// Control structures that should have spacing
IfStatement (node) {
checkControlStructureSpacing(node);
},
WhileStatement (node) {
checkControlStructureSpacing(node);
},
DoWhileStatement (node) {
checkControlStructureSpacing(node);
},
SwitchStatement (node) {
checkControlStructureSpacing(node);
},
CatchClause (node) {
if ( node.param ) {
checkControlStructureSpacing(node);
}
},
// For loops need special handling
ForStatement (node) {
checkForLoopSpacing(node);
},
ForInStatement (node) {
checkForLoopSpacing(node);
},
ForOfStatement (node) {
checkForLoopSpacing(node);
},
// Function calls that should NOT have spacing
CallExpression (node) {
checkFunctionCallSpacing(node);
},
NewExpression (node) {
if ( node.arguments.length > 0 || sourceCode.getLastToken(node).value === ')' ) {
checkFunctionCallSpacing(node);
}
},
};
},
};
-86
View File
@@ -1,86 +0,0 @@
import tseslintPlugin from '@typescript-eslint/eslint-plugin';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
const backendLanguageOptions = {
globals: {
// Current, intentionally supported globals
extension: 'readonly',
config: 'readonly',
global_config: 'readonly',
// Older not entirely ideal globals
use: 'readonly', // <-- older import mechanism
def: 'readonly', // <-- older import mechanism
kv: 'readonly', // <-- should be passed/imported
ll: 'readonly', // <-- questionable
// Language/environment globals
...globals.node,
},
};
const mandatoryRules = {
'no-undef': 'error',
'no-use-before-define': ['error', {
'functions': false,
}],
'no-invalid-this': 'warn',
};
export default defineConfig([
{
ignores: [
'src/backend/src/modules/apps/AppInformationService.js', // TEMPORARY - SHOULD BE FIXED!
'src/backend/src/services/worker/WorkerService.js', // TEMPORARY - SHOULD BE FIXED!
'src/backend/src/public/**/*', // We may be able to delete this! I don't think it's used
// These files run in the worker environment, so these rules don't apply
'src/backend/src/services/worker/dist/**/*.{js,cjs,mjs}',
'src/backend/src/services/worker/src/**/*.{js,cjs,mjs}',
'src/backend/src/services/worker/template/puter-portable.js',
],
},
{
plugins: {
'@typescript-eslint': tseslintPlugin,
},
},
{
files: [
'src/backend/**/*.{js,mjc,cjs}',
'extensions/**/*.{js,mjc,cjs}',
],
ignores: [
'src/backend/src/services/database/sqlite_setup/**/*.js',
],
rules: mandatoryRules,
languageOptions: {
...backendLanguageOptions,
},
},
{
files: [
'src/backend/src/services/database/sqlite_setup/**/*.js',
],
rules: mandatoryRules,
languageOptions: {
globals: {
read: 'readonly',
write: 'readonly',
log: 'readonly',
...globals.node,
},
},
},
{
files: [
'src/backend/**/*.{ts}',
'extensions/**/*.{ts}',
],
rules: mandatoryRules,
languageOptions: {
...backendLanguageOptions,
},
},
]);
-37
View File
@@ -1,37 +0,0 @@
import ruleComposer from 'eslint-rule-composer';
// Adjust this require to match the package you use for the rule.
// For eslint-stylistic v2+ the package is "@stylistic/eslint-plugin"
import stylistic from '@stylistic/eslint-plugin';
const baseRule = stylistic.rules['space-unary-ops'];
// unwrap nested parentheses
function unwrapParens (node) {
let n = node;
while ( n && n.type === 'ParenthesizedExpression' ) n = n.expression;
return n;
}
function isTopLevelBangInIfTest (node) {
if ( !node || node.type !== 'UnaryExpression' || node.operator !== '!' ) return false;
// Walk up through ancestors manually using .parent (safe in ESLint)
let current = node;
let parent = current.parent;
// Skip ParenthesizedExpression layers
while ( parent && parent.type === 'ParenthesizedExpression' ) {
current = parent;
parent = parent.parent;
}
return parent && parent.type === 'IfStatement' && unwrapParens(parent.test) === node;
}
// Filter out ONLY the reports for top-level ! inside if(...) condition
export default ruleComposer.filterReports(baseRule, (problem, context) => {
const { node } = problem;
// If this particular report is about a top-level ! in an if(...) test,
// suppress it. Otherwise, keep the original report.
return !isTopLevelBangInIfTest(node, context);
});
-20
View File
@@ -1,20 +0,0 @@
/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import backend from '@heyputer/backend';
export default backend;
View File
Whitespace-only changes.
-11
View File
@@ -1,11 +0,0 @@
# Extension System Development Guide
## Where to find documentation
### Here
Documentation for extensions is [here](src/backend/doc/extensions/README.md).
### Not Here
Outdated documentation for extensions is [here](../doc/contributors/extensions/README.md).
This documentation may include some topics that are missing from the current documentation. Eventually those topics should be updated and transferred to the current documentation so that this documentation may be removed.
-207
View File
@@ -1,207 +0,0 @@
import type APIError from '@heyputer/backend/src/api/APIError.js';
import type query from '@heyputer/backend/src/om/query/query';
import type { Actor } from '@heyputer/backend/src/services/auth/Actor.js';
import type { ServicesMap } from '@heyputer/backend/src/services/BaseService.d.ts';
import type { BaseDatabaseAccessService } from '@heyputer/backend/src/services/database/BaseDatabaseAccessService.d.ts';
import type { DynamoKVStore } from '@heyputer/backend/src/services/repositories/DynamoKVStore/DynamoKVStore.ts';
import type { IUser } from '@heyputer/backend/src/services/User.js';
import type { Context } from '@heyputer/backend/src/util/context.js';
import type kvjs from '@heyputer/kv.js';
import type { RequestHandler } from 'express';
import type { Cluster } from 'ioredis';
import type FSNodeContext from '../src/backend/src/filesystem/FSNodeContext.js';
import type helpers from '../src/backend/src/helpers.js';
import type { ICompleteArguments } from '../src/backend/src/services/ai/chat/providers/types.ts';
import type * as ExtensionControllerExports from './ExtensionController/src/ExtensionController.ts';
import type { s3ClientProvider } from '../src/backend/src/clients/s3/s3ClientProvider.js';
declare global {
namespace Express {
interface Request {
services: {
get: <T extends keyof ServicesMap | (string & {})>(
string: T,
) => T extends keyof ServicesMap ? ServicesMap[T] : unknown;
};
actor?: Actor;
rawBody: Buffer;
/** @deprecated use actor instead */
user: IUser;
}
}
}
export type { Cluster } from 'ioredis';
export interface EndpointOptions {
allowedMethods?: string[];
subdomain?: string;
noauth?: boolean;
mw?: RequestHandler[];
otherOpts?: Record<string, unknown> & {
json?: boolean;
noReallyItsJson?: boolean;
};
}
// Driver interface types
interface ParameterDefinition {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
optional: boolean;
}
interface MethodDefinition {
description: string;
parameters: Record<string, ParameterDefinition>;
}
interface DriverInterface {
description: string;
methods: Record<string, MethodDefinition>;
}
export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
export type AddRouteFunction = (
path: string,
options: EndpointOptions,
handler: RequestHandler,
) => void;
export type RouterMethods = {
[K in HttpMethod]: {
(path: string, options: EndpointOptions, handler: RequestHandler): void;
(path: string, handler: RequestHandler, options?: EndpointOptions): void;
};
};
interface CoreRuntimeModule {
util: {
helpers: typeof helpers;
};
redisClient: Cluster;
kvjs: kvjs
s3ClientProvider: typeof s3ClientProvider;
Context: typeof Context;
APIError: typeof APIError;
}
interface FilesystemModule {
FSNodeContext: FSNodeContext;
selectors: unknown;
}
export interface ExtensionEventTypeMap {
'metering:registerAvailablePolicies': {
availablePolicies: unknown[]
},
'create.drivers': {
createDriver: (interface: string, service: string, executors: any) => any;
};
'create.permissions': {
grant_to_everyone: (permission: string) => void;
grant_to_users: (permission: string) => void;
};
'create.interfaces': {
createInterface: (interface: string, interfaces: DriverInterface) => void;
};
'puter.gui.addons': {
bodyContent: string;
headContent: string;
guiParams: {
env: string;
app_origin: string;
api_origin: string;
gui_origin: string;
asset_dir: string;
launch_options: unknown;
app_name_regex: RegExp;
app_name_max_length: number;
app_title_max_length: number;
hosting_domain: string;
subdomain_regex: RegExp;
subdomain_max_length: number;
domain: string;
protocol: string;
api_base_url: string;
app?: { name: string, uid: string } & Record<string, unknown>;
[key: string]: unknown;
};
};
'app.changed': {
app_uid: string;
action: 'updated' | 'deleted';
};
'app.privateAccess.check': {
appUid: string;
userUid?: string | null;
requestHost?: string;
requestPath?: string;
result: {
allowed: boolean;
redirectUrl?: string;
reason?: string;
checkedBy?: string;
};
};
'app.privateAccess.resolveLaunch': {
appUid: string;
appName?: string;
userUid?: string | null;
source?: string;
args?: Record<string, unknown>;
result: {
hasAccess: boolean;
fallbackAppName?: string;
fallbackArgs?: Record<string, unknown>;
reason?: string;
checkedBy?: string;
};
};
'ai.prompt.validate': {
actor: Actor;
actor,
completionId: string,
allow: boolean,
intended_service: string,
parameters: ICompleteArguments
}
'outer.cacheUpdate': { cacheKey: string | string[], ttlSeconds?: number, data?: unknown }
}
interface Extension extends RouterMethods {
exports: Record<string, unknown>;
span: (<T>(label: string, fn: () => T) => () => T) & {
run<T>(label: string, fn: () => T): T;
run<T>(fn: () => T): T;
};
config: Record<string | number | symbol, any>;
on<E extends keyof ExtensionEventTypeMap>(
name: E,
listener: (event: ExtensionEventTypeMap[E], metadata?: { from_outside?: boolean }) => void | Promise<void>
): void;
on<T>(name: string, listener: (event: T, metadata?: { from_outside?: boolean }) => void | Promise<void>): void
import(module: 'data'): {
db: BaseDatabaseAccessService;
kv: DynamoKVStore;
cache: Cluster;
s3ClientProvider: typeof s3ClientProvider;
};
import(module: 'core'): CoreRuntimeModule;
import(module: 'fs'): FilesystemModule;
import(module: 'query'): typeof query;
import(module: 'extensionController'): typeof ExtensionControllerExports;
import<T extends `service:${keyof ServicesMap}` | (string & {})>(
module: T
): T extends `service:${infer R extends keyof ServicesMap}`
? ServicesMap[R]
: unknown;
}
declare global {
// Declare the extension variable
const extension: Extension;
const config: Record<string | number | symbol, any>;
const global_config: Record<string | number | symbol, unknown>;
}
-138
View File
@@ -1,138 +0,0 @@
const { Eq } = extension.import('query');
const { db } = extension.import('data');
const { APIError, Context } = extension.import('core');
const app_es = extension.import('service:es:app') as any;
const svc_permission = extension.import('service:permission') as any;
const DEFAULT_LIMIT = 100;
const MAX_LIMIT = 1000;
const MAX_OFFSET = 100_000;
const parseIntegerParam = (
value: unknown,
{
key,
min,
max,
fallback,
}: { key: string, min: number, max: number, fallback: number },
) => {
if ( value === undefined || value === null ) return fallback;
const parsed = typeof value === 'number'
? value
: (typeof value === 'string' && value.trim() !== ''
? Number(value)
: Number.NaN);
if ( !Number.isFinite(parsed) || !Number.isInteger(parsed) ) {
throw APIError.create('field_invalid', undefined, {
key,
expected: `an integer between ${min} and ${max}`,
got: value,
});
}
if ( parsed < min || parsed > max ) {
throw APIError.create('field_invalid', undefined, {
key,
expected: `an integer between ${min} and ${max}`,
got: parsed,
});
}
return parsed;
};
extension.on('create.interfaces', (event) => {
event.createInterface('app-telemetry', {
description: 'Provides methods for getting app telemetry',
methods: {
get_users: {
description: 'Returns users who have used your app',
parameters: {
app_uuid: {
type: 'string',
optional: false,
},
limit: {
type: 'number',
optional: true,
},
offset: {
type: 'number',
optional: true,
},
},
},
user_count: {
description: 'Returns number of users who have used your app',
parameters: {
app_uuid: {
type: 'string',
optional: false,
},
},
},
},
});
});
extension.on('create.drivers', event => {
event.createDriver('app-telemetry', 'app-telemetry', {
async get_users ({ app_uuid, limit, offset }: { app_uuid: string, limit?: number, offset?: number }) {
const safeLimit = parseIntegerParam(limit, {
key: 'limit',
min: 1,
max: MAX_LIMIT,
fallback: DEFAULT_LIMIT,
});
const safeOffset = parseIntegerParam(offset, {
key: 'offset',
min: 0,
max: MAX_OFFSET,
fallback: 0,
});
// first lets make sure executor owns this app
const [result] = (await app_es.select({ predicate: new Eq({ key: 'uid', value: app_uuid }) }));
if ( ! result ) {
throw APIError.create('permission_denied');
}
if ( ! (await svc_permission.check(Context.get('actor'), `apps-of-user:${result.values_.owner.uuid}:write`, { no_cache: true })) ) {
throw APIError.create('permission_denied');
}
// Fetch and return users
const users: Array<{ username: string, uuid: string }> = await db.read(
`SELECT user.username, user.uuid FROM user_to_app_permissions
INNER JOIN user ON user_to_app_permissions.user_id = user.id
WHERE permission = 'flag:app-is-authenticated' AND app_id=? ORDER BY (dt IS NOT NULL), dt, user_id LIMIT ? OFFSET ?`,
[result.private_meta.mysql_id, safeLimit, safeOffset],
);
return users.map(e => {
return { user: e.username, user_uuid: e.uuid };
});
},
async user_count ({ app_uuid }: { app_uuid: string }) {
// first lets make sure executor owns this app
const [result] = (await app_es.select({ predicate: new Eq({ key: 'uid', value: app_uuid }) }));
if ( ! result ) {
throw APIError.create('permission_denied');
}
// Fetch and return authenticated user count
const [data] = await db.read(
`SELECT count(*) FROM user_to_app_permissions
WHERE permission = 'flag:app-is-authenticated' AND app_id=?;`,
[result.private_meta.mysql_id],
);
const count = data['count(*)'];
return count;
},
});
});
extension.on('create.permissions', (event) => {
event.grant_to_everyone('service:app-telemetry:ii:app-telemetry');
});
-1
View File
@@ -1 +0,0 @@
import '../api.js';
-12
View File
@@ -1,12 +0,0 @@
{
"name": "@heyputer/app-telemetry",
"main": "app-user-count.js",
"type": "module",
"scripts": {
"postinstall": "tsc --noCheck",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}
-27
View File
@@ -1,27 +0,0 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "nodenext",
"moduleResolution": "nodenext",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"sourceMap": true,
},
"include": [
"./**/*.ts",
"./**/*.d.ts"
],
"exclude": [
"**/*.test.ts",
"**/*.spec.ts",
"**/test/**",
"**/tests/**",
"node_modules",
"dist",
"*.js"
]
}
+139
View File
@@ -0,0 +1,139 @@
import { v4 as uuidv4 } from 'uuid';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { runWithContext } from '../src/backend/core/context.ts';
import { PuterServer } from '../src/backend/server.ts';
import { setupTestServer } from '../src/backend/testUtil.ts';
// Importing the module registers the `appTelemetry` driver into the shared
// extensionStore, so `setupTestServer` instantiates it on `server.drivers`.
import { AppTelemetryDriver } from './appTelemetry.ts';
let server: PuterServer;
let driver: AppTelemetryDriver;
// `get_users` reads `Context.get('actor')` for the ownership gate. Wrap
// actor-dependent calls in `runWithContext` so the ALS lookup resolves.
const callWithActor = <T>(
actor: { user: { uuid: string; id?: number } } | undefined,
fn: () => Promise<T>,
) => runWithContext({ actor }, fn);
beforeAll(async () => {
server = await setupTestServer();
driver = (server.drivers as unknown as Record<string, AppTelemetryDriver>)
.appTelemetry;
// Guard: the driver must have been wired onto the server. If this is
// undefined the extension didn't register, and every test below would
// fail with a confusing "cannot read property of undefined".
expect(driver).toBeInstanceOf(AppTelemetryDriver);
});
afterAll(async () => {
await server.shutdown();
});
const seedOwnedApp = async (prefix: string) => {
const owner = await server.stores.user.create({
username: `${prefix}_${Math.random().toString(36).slice(2, 8)}`,
uuid: uuidv4(),
password: 'x',
email: null,
});
const slug = Math.random().toString(36).slice(2, 8);
const app = await server.stores.app.create(
{
name: `${prefix}_${slug}`,
title: `${prefix} ${slug}`,
index_url: `https://example.com/${slug}`,
},
{ ownerUserId: owner.id as number },
);
return { owner, app: app! };
};
describe('appTelemetry driver — get_users', () => {
it('throws HttpError(400) when app_uuid is missing', async () => {
await expect(driver.get_users({})).rejects.toMatchObject({
statusCode: 400,
message: expect.stringContaining('app_uuid'),
});
});
it('throws HttpError(400) for a non-numeric limit', async () => {
await expect(
driver.get_users({ app_uuid: 'app-anything', limit: 'banana' }),
).rejects.toMatchObject({
statusCode: 400,
message: expect.stringContaining('limit'),
});
});
it('throws HttpError(400) when offset exceeds the allowed maximum', async () => {
await expect(
driver.get_users({ app_uuid: 'app-anything', offset: 1_000_001 }),
).rejects.toMatchObject({ statusCode: 400 });
});
it('throws HttpError(404) when the app cannot be found', async () => {
await expect(
driver.get_users({ app_uuid: 'app-does-not-exist' }),
).rejects.toMatchObject({
statusCode: 404,
message: 'App not found',
});
});
it('throws HttpError(403) when the caller does not own the app', async () => {
// Seed an owner + their app, then call as a *different* user. The
// real permission service should reject: the caller has no
// `apps-of-user:<owner>:write`.
const { app } = await seedOwnedApp('owner');
const intruder = await server.stores.user.create({
username: `intruder_${Math.random().toString(36).slice(2, 8)}`,
uuid: uuidv4(),
password: 'x',
email: null,
});
await callWithActor(
{ user: { uuid: intruder.uuid, id: intruder.id as number } },
async () => {
await expect(
driver.get_users({ app_uuid: app.uid }),
).rejects.toMatchObject({
statusCode: 403,
message: 'Permission denied',
});
},
);
});
it('returns an empty list for an owned app with no authenticated users', async () => {
const { owner, app } = await seedOwnedApp('owner2');
const result = await callWithActor(
{ user: { uuid: owner.uuid, id: owner.id as number } },
() => driver.get_users({ app_uuid: app.uid }),
);
expect(result).toEqual([]);
});
});
describe('appTelemetry driver — user_count', () => {
it('throws HttpError(400) when app_uuid is missing', async () => {
await expect(driver.user_count({})).rejects.toMatchObject({
statusCode: 400,
});
});
it('throws HttpError(404) for an unknown app_uuid', async () => {
await expect(
driver.user_count({ app_uuid: 'app-not-here' }),
).rejects.toMatchObject({ statusCode: 404 });
});
it('returns 0 for an app with no authenticated users', async () => {
const { app } = await seedOwnedApp('appcount');
await expect(driver.user_count({ app_uuid: app.uid })).resolves.toBe(0);
});
});
+139
View File
@@ -0,0 +1,139 @@
import { Context } from '@heyputer/backend/src/core';
import type { Actor } from '@heyputer/backend/src/core/actor';
import { HttpError } from '@heyputer/backend/src/core/http';
import { PuterDriver } from '@heyputer/backend/src/drivers/types';
import { extension } from '@heyputer/backend/src/extensions';
// App-telemetry lets an app owner enumerate the users who have
// authenticated into their app. v1 shipped this as a driver on the
// `app-telemetry` interface (methods `get_users` / `user_count`) and
// puter-js's `puter.apps(...).getUsers()` still calls it that way
// (`puter.drivers.call('app-telemetry', 'app-telemetry', 'get_users', …)`).
// This is the v2 port of that driver — same interface/method/return shapes
// so existing puter-js callers work unchanged.
const DEFAULT_LIMIT = 100;
const MAX_LIMIT = 1000;
const MAX_OFFSET = 100_000;
const parseIntParam = (
value: unknown,
{
key,
min,
max,
fallback,
}: { key: string; min: number; max: number; fallback: number },
): number => {
if (value === undefined || value === null) return fallback;
const parsed =
typeof value === 'number'
? value
: typeof value === 'string' && value.trim() !== ''
? Number(value)
: NaN;
if (
!Number.isFinite(parsed) ||
!Number.isInteger(parsed) ||
parsed < min ||
parsed > max
) {
throw new HttpError(
400,
`${key} must be an integer between ${min} and ${max}`,
);
}
return parsed;
};
/**
* Driver exposing the `app-telemetry` interface.
*
* The `/drivers/call` permission gate checks
* `service:app-telemetry:ii:app-telemetry`, which every actor already holds
* via the blanket `service` grant (hardcoded-permissions.js +
* `default_implicit_user_app_permissions`). The real authorization — "is the
* caller the app owner?" — is enforced inside `get_users` below, exactly as
* v1 did.
*/
export class AppTelemetryDriver extends PuterDriver {
readonly driverInterface = 'app-telemetry';
readonly driverName = 'app-telemetry';
readonly isDefault = true;
/** Users who have authenticated into the given app (owner-only). */
async get_users({
app_uuid,
limit,
offset,
}: {
app_uuid?: string;
limit?: unknown;
offset?: unknown;
} = {}): Promise<Array<{ user: string; user_uuid: string }>> {
if (!app_uuid) throw new HttpError(400, 'Missing `app_uuid`');
const safeLimit = parseIntParam(limit, {
key: 'limit',
min: 1,
max: MAX_LIMIT,
fallback: DEFAULT_LIMIT,
});
const safeOffset = parseIntParam(offset, {
key: 'offset',
min: 0,
max: MAX_OFFSET,
fallback: 0,
});
const app = await this.stores.app.getByUid(app_uuid);
if (!app) throw new HttpError(404, 'App not found');
// The `apps-of-user:<uuid>:write` implicator keys on the owner's
// UUID, not the numeric id. Look up the owner explicitly — the raw
// app row only carries `owner_user_id`. (v1 got the owner for free
// because its entity-storage layer eager-joined the owner row.)
const ownerId = (app as { owner_user_id?: number }).owner_user_id;
if (!ownerId) throw new HttpError(404, 'App owner not found');
const owner = await this.stores.user.getById(ownerId);
if (!owner?.uuid) throw new HttpError(404, 'App owner not found');
const actor = Context.get('actor');
if (!actor) throw new HttpError(401, 'Authentication required');
const ownsApp = await this.services.permission
.check(actor as Actor, `apps-of-user:${owner.uuid}:write`)
.catch(() => false);
if (!ownsApp) throw new HttpError(403, 'Permission denied');
const users = (await this.clients.db.read(
`SELECT u.username, u.uuid FROM user_to_app_permissions p
INNER JOIN ${this.clients.db.quoteIdentifier('user')} u ON p.user_id = u.id
WHERE p.permission = 'flag:app-is-authenticated' AND p.app_id = ?
ORDER BY (p.dt IS NOT NULL), p.dt, p.user_id
LIMIT ? OFFSET ?`,
[(app as { id: number }).id, safeLimit, safeOffset],
)) as Array<{ username: string; uuid: string }>;
return users.map((e) => ({ user: e.username, user_uuid: e.uuid }));
}
/** Count of users who have authenticated into the given app. */
async user_count({
app_uuid,
}: { app_uuid?: string } = {}): Promise<number> {
if (!app_uuid) throw new HttpError(400, 'Missing `app_uuid`');
const app = await this.stores.app.getByUid(app_uuid);
if (!app) throw new HttpError(404, 'App not found');
const [row] = (await this.clients.db.read(
`SELECT COUNT(*) AS n FROM user_to_app_permissions
WHERE permission = 'flag:app-is-authenticated' AND app_id = ?`,
[(app as { id: number }).id],
)) as Array<{ n: number }>;
return row?.n ?? 0;
}
}
extension.registerDriver('appTelemetry', AppTelemetryDriver);
-34
View File
@@ -1,34 +0,0 @@
//@extension priority -10000
const { redisClient, kvjs, s3ClientProvider } = extension.import('core');
const svc_database = extension.import('service:database');
const svc_kvstore = extension.import('service:puter-kvstore');
// Methods on the object from `.as()` come from TraitsFeature.js,
// and they are already bound to their respective instance.
const simplified_kv = { ...svc_kvstore.as('puter-kvstore') };
const original_get = simplified_kv.get;
const original_set = simplified_kv.set;
simplified_kv.get = (...a) => {
if ( typeof a[0] === 'string' ) {
return original_get({ key: a[0] });
}
return original_get(...a);
};
simplified_kv.set = (...a) => {
if ( typeof a[0] === 'string' ) {
return original_set({ key: a[0], value: a[1] });
}
return original_set(...a);
};
extension.exports = {
db: svc_database.get(),
kv: simplified_kv,
cache: redisClient,
kvjs: kvjs,
s3ClientProvider,
};
+421
View File
@@ -0,0 +1,421 @@
import { extension } from '@heyputer/backend/src/extensions';
import { PuterService } from '@heyputer/backend/src/services/types.js';
import { nativeImport } from '@heyputer/backend/src/util/nativeImport.js';
import { spawn, type ChildProcess } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
const requireFromHere = createRequire(__filename);
const webpack = requireFromHere('webpack') as typeof import('webpack');
type EnvFactory = (args: {
global_config: Record<string, unknown> | null;
}) => string | undefined;
type EnvMap = Record<string, string | undefined | EnvFactory>;
type CommandEntry = {
name: string;
directory: string;
command: string;
args?: string[];
env?: EnvMap;
};
type WebpackEntry = {
name?: string;
directory: string;
env?: EnvMap;
onConfig?: (config: Record<string, unknown>) => void;
};
type WebpackStats = {
hasErrors: () => boolean;
toJson: (options: Record<string, unknown>) => {
errors?: Array<{ message?: string }>;
warnings?: Array<{ message?: string }>;
};
};
type DevWatcherConfig = {
enabled?: boolean;
root?: string;
commands?: CommandEntry[];
webpack?: WebpackEntry[];
ready_delay_ms?: number;
};
class ProxyLogger {
#buffer = '';
constructor(private readonly log: (line: string) => void) {}
attach(stream: NodeJS.ReadableStream | null): void {
if (!stream) return;
stream.on('data', (chunk) => {
this.#buffer += chunk.toString();
let lineEndIndex = this.#buffer.indexOf('\n');
while (lineEndIndex !== -1) {
const line = this.#buffer.substring(0, lineEndIndex);
this.log(line);
this.#buffer = this.#buffer.substring(lineEndIndex + 1);
lineEndIndex = this.#buffer.indexOf('\n');
}
});
stream.on('end', () => {
if (this.#buffer.length) {
this.log(this.#buffer);
this.#buffer = '';
}
});
}
}
const findPackageRoot = (): string => {
let dir = __dirname;
for (;;) {
if (
existsSync(path.join(dir, 'package.json')) &&
existsSync(path.join(dir, 'src', 'gui')) &&
existsSync(path.join(dir, 'src', 'puter-js'))
) {
return dir;
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return path.resolve(__dirname, '..', '..');
};
const resolveFromRoot = (root: string, value: string): string =>
path.isAbsolute(value) ? value : path.resolve(root, value);
const defaultWebpackEntries: WebpackEntry[] = [
{
name: 'puter.js',
directory: 'src/puter-js',
onConfig: (config) => {
const output = (config.output ?? {}) as Record<string, unknown>;
output.filename = 'puter.dev.js';
config.output = output;
config.devtool = 'source-map';
},
env: {
PUTER_ORIGIN: ({ global_config }) =>
String(global_config?.origin ?? ''),
PUTER_API_ORIGIN: ({ global_config }) =>
String(global_config?.api_base_url ?? ''),
},
},
{
name: 'gui',
directory: 'src/gui',
},
];
class DevWatcherService extends PuterService {
#processes: Array<{ name: string; proc: ChildProcess }> = [];
#watchers: ReturnType<ReturnType<typeof webpack>['watch']>[] = [];
#started = false;
#packageRoot = findPackageRoot();
override async onServerStart(): Promise<void> {
if (!this.#shouldStart()) return;
if (this.#started) return;
this.#started = true;
const devwatch = this.#devwatchConfig();
const root = resolveFromRoot(
this.#packageRoot,
devwatch.root ?? this.#packageRoot,
);
const commands = devwatch.commands ?? [];
const webpackEntries = devwatch.webpack ?? defaultWebpackEntries;
console.log(`[devwatch] starting watchers from ${root}`);
await Promise.all([
...commands.map((entry) => this.#startCommand(root, entry)),
...webpackEntries.map((entry) =>
this.#startWebpackWatcher(root, entry),
),
]);
const readyDelayMs = devwatch.ready_delay_ms ?? 5000;
if (readyDelayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, readyDelayMs));
}
}
override async onServerShutdown(): Promise<void> {
await Promise.all(
this.#watchers.map(
(watcher) =>
new Promise<void>((resolve) => {
watcher?.close((err) => {
if (err) {
console.warn(
'[devwatch] failed to close webpack watcher:',
err,
);
}
resolve();
});
}),
),
);
this.#watchers = [];
for (const { name, proc } of this.#processes) {
if (proc.exitCode !== null || proc.killed) continue;
console.log(`[devwatch:${name}] stopping process`);
proc.kill();
}
this.#processes = [];
}
#devwatchConfig(): DevWatcherConfig {
const raw = (this.config as Record<string, unknown>).devwatch;
return raw && typeof raw === 'object' ? (raw as DevWatcherConfig) : {};
}
#shouldStart(): boolean {
const config = this.config as Record<string, unknown>;
const devwatch = this.#devwatchConfig();
if (config.no_devwatch === true) return false;
if (devwatch.enabled === false) return false;
if (devwatch.enabled === true) return true;
return this.config.env === 'dev';
}
async #startCommand(root: string, entry: CommandEntry): Promise<void> {
const fullpath = resolveFromRoot(root, entry.directory);
console.log(`[devwatch] starting ${entry.name} in ${fullpath}`);
const proc = spawn(entry.command, entry.args ?? [], {
shell: true,
cwd: fullpath,
env: {
...process.env,
...this.#evaluateEnv(entry.env),
},
});
this.#processes.push({ name: entry.name, proc });
new ProxyLogger((line) =>
console.log(`[devwatch:${entry.name}:1] ${line}`),
).attach(proc.stdout);
new ProxyLogger((line) =>
console.warn(`[devwatch:${entry.name}:2] ${line}`),
).attach(proc.stderr);
proc.on('exit', () => {
console.log(
`[devwatch:${entry.name}:exit] process exited (${proc.exitCode})`,
);
this.#processes = this.#processes.filter(
(instance) => instance.proc !== proc,
);
});
}
async #startWebpackWatcher(
root: string,
entry: WebpackEntry,
): Promise<void> {
const directory = entry.directory;
let { configjsPath: webpackConfigPath, moduleType } = this.#getConfigJs(
{
root,
directory,
configIsFor: 'webpack',
possibleConfigNames: [
['webpack.config.js', 'package.json'],
['webpack.config.cjs', 'commonjs'],
['webpack.config.mjs', 'module'],
],
},
);
let webpackConfig = await this.#withEnv(entry.env, async () => {
if (moduleType === 'module') {
webpackConfigPath = pathToFileURL(webpackConfigPath).href;
const imported = await nativeImport<{ default?: unknown }>(
webpackConfigPath,
);
return imported.default ?? imported;
}
return requireFromHere(webpackConfigPath);
});
if (typeof webpackConfig === 'function') {
webpackConfig = await this.#withEnv(entry.env, () =>
(webpackConfig as () => unknown)(),
);
}
this.#normalizeWebpackContext(root, directory, webpackConfig);
if (entry.onConfig) {
entry.onConfig(webpackConfig as Record<string, unknown>);
}
const compiler = webpack(
webpackConfig as Parameters<typeof webpack>[0],
);
const watcher = compiler.watch({}, (err, stats) => {
this.#handleWebpackUpdate(entry, err, stats);
});
this.#watchers.push(watcher);
}
#getConfigJs(args: {
root: string;
directory: string;
configIsFor: string;
possibleConfigNames: Array<
[string, 'package.json' | 'commonjs' | 'module']
>;
}): {
configjsPath: string;
moduleType: 'commonjs' | 'module';
} {
const { root, directory, configIsFor, possibleConfigNames } = args;
let configjsPath: string | undefined;
let moduleType: 'package.json' | 'commonjs' | 'module' | undefined;
for (const [configName, supposedModuleType] of possibleConfigNames) {
const supposedPath = path.join(root, directory, configName);
if (existsSync(supposedPath)) {
configjsPath = supposedPath;
moduleType = supposedModuleType;
break;
}
}
if (!configjsPath || !moduleType) {
throw new Error(
`could not find ${configIsFor} config for: ${directory}`,
);
}
if (moduleType === 'package.json') {
const packageJSONPath = path.join(root, directory, 'package.json');
const packageJSONObject = JSON.parse(
readFileSync(packageJSONPath, 'utf8'),
) as { type?: 'commonjs' | 'module' };
moduleType = packageJSONObject.type ?? 'module';
}
return {
configjsPath,
moduleType,
};
}
async #withEnv<T>(env: EnvMap | undefined, fn: () => T | Promise<T>) {
if (!env) return fn();
const oldEnv = process.env;
process.env = {
...oldEnv,
...this.#evaluateEnv(env),
};
try {
return await fn();
} finally {
process.env = oldEnv;
}
}
#evaluateEnv(env: EnvMap | undefined): Record<string, string> {
const out: Record<string, string> = {};
if (!env) return out;
for (const [key, value] of Object.entries(env)) {
try {
const evaluated =
typeof value === 'function'
? value({
global_config: this.config as Record<
string,
unknown
>,
})
: value;
if (evaluated) out[key] = String(evaluated);
} catch (e) {
const msg = (e as Error).message;
if (
!msg.includes('Cannot read properties of null') &&
!msg.includes('Cannot read properties of undefined')
) {
console.warn(
`[devwatch] could not evaluate env function for ${key}: ${msg}`,
);
}
}
}
return out;
}
#normalizeWebpackContext(
root: string,
directory: string,
webpackConfig: unknown,
): void {
const configs = Array.isArray(webpackConfig)
? webpackConfig
: [webpackConfig];
for (const config of configs) {
if (!config || typeof config !== 'object') continue;
const obj = config as Record<string, unknown>;
obj.context = obj.context
? path.resolve(path.join(root, directory), String(obj.context))
: path.join(root, directory);
}
}
#handleWebpackUpdate(
entry: WebpackEntry,
err: Error | null | undefined,
stats: WebpackStats | undefined,
): void {
const name = entry.name ?? entry.directory;
const firstEventKey = `__devwatch_first_${entry.directory}`;
const firstEvent = !(entry as Record<string, unknown>)[firstEventKey];
(entry as Record<string, unknown>)[firstEventKey] = true;
if (err || stats?.hasErrors()) {
const info = stats?.toJson({
all: false,
errors: true,
warnings: true,
});
console.error(
`[devwatch] error information: ${name} using Webpack`,
{
err: err ? err.message : null,
errors: info?.errors?.map((e) => e.message) ?? [],
warnings: info?.warnings?.map((w) => w.message) ?? [],
},
);
console.error(`[devwatch] failed to update ${name} using Webpack`);
return;
}
if (!firstEvent) {
console.log(`[devwatch] updated ${name} using Webpack`);
}
}
}
extension.registerService('devWatcher', DevWatcherService);
@@ -1,22 +0,0 @@
{
"name": "@puter/extension-controller",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"type": "module",
"scripts": {
"postinstall": "tsc --noCheck"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^24.9.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
},
"dependencies": {
"http-status-codes": "^2.3.0",
"stripe": "^19.1.0"
}
}
@@ -1,3 +0,0 @@
{
"priority": -10
}
@@ -1,238 +0,0 @@
import type { NextFunction, Request, Response } from 'express';
import { StatusCodes } from 'http-status-codes';
import type {
EndpointOptions,
HttpMethod,
RouterMethods,
} from '../../api.d.ts';
declare const extension: Partial<Record<HttpMethod, RouterMethods[HttpMethod]>>;
/**
* Class decorator to set prefix on prototype and register routes on instantiation
* @argument prefix - prefix for all routes under the class
* @argument [adminUsernames] - gate all routes behind admin username check
*/
export const Controller = (
prefix: string,
adminUsernames?: string[],
allowedAppIds?: string[],
): ClassDecorator => {
return (target: Function) => {
target.prototype.__controllerPrefix = prefix;
target.prototype.__allowedAppIds = allowedAppIds;
target.prototype.__adminUsernames = adminUsernames
? [...adminUsernames, 'admin', 'system']
: undefined;
};
};
/**
* Method decorator factory that collects route metadata
*/
interface RouteMeta {
method: HttpMethod;
path: string;
options?: EndpointOptions | undefined;
handler: (req: Request, res: Response, next: NextFunction) => void | Promise<void>;
adminUsernames?: string[];
allowedAppIds?: string[];
}
const createMethodDecorator = (method: HttpMethod) => {
return <This>(
path: string,
routeOptions?: EndpointOptions & { allowedAppIds?: string[] },
adminUsernames?: string[],
) => {
const { allowedAppIds, ...options } = routeOptions ?? {};
return (
target: (req: Request, res: Response, next: NextFunction) => void | Promise<void>,
_context: ClassMethodDecoratorContext<
This,
(
this: This,
...args: [req: Request, res: Response, next: NextFunction]
) => void | Promise<void>
>,
) => {
_context.addInitializer(function () {
// eslint-disable-next-line no-invalid-this
const proto = Object.getPrototypeOf(this); // will be bound to class
if ( ! proto.__routes ) {
proto.__routes = [];
}
proto.__routes.push({
method,
path,
options: options as EndpointOptions | undefined,
adminUsernames: adminUsernames
? [...adminUsernames, 'admin', 'system']
: undefined,
allowedAppIds,
handler: target,
});
});
};
};
};
// HTTP method decorators
export const Get = createMethodDecorator('get');
export const Post = createMethodDecorator('post');
export const Put = createMethodDecorator('put');
export const Delete = createMethodDecorator('delete');
// TODO DS: add others as needed (patch, etc)
interface HttpErrorOptions {
cause?: unknown;
legacyCode?: string;
code?: string;
fields?: Record<string, unknown>;
}
const isHttpErrorOptions = (value: unknown): value is HttpErrorOptions => {
if ( !value || typeof value !== 'object' || Array.isArray(value) ) {
return false;
}
return (
Object.prototype.hasOwnProperty.call(value, 'cause')
|| Object.prototype.hasOwnProperty.call(value, 'legacyCode')
|| Object.prototype.hasOwnProperty.call(value, 'code')
|| Object.prototype.hasOwnProperty.call(value, 'fields')
);
};
export class HttpError extends Error {
statusCode: number;
legacyCode?: string;
code?: string;
fields?: Record<string, unknown>;
constructor (
statusCode: StatusCodes,
message: string,
causeOrOptions?: unknown,
legacyCode?: string,
) {
const options = isHttpErrorOptions(causeOrOptions)
? causeOrOptions
: undefined;
const cause = options
? options.cause
: causeOrOptions;
const resolvedLegacyCode = legacyCode ?? options?.legacyCode;
const code = options?.code;
super(
`${statusCode} - ${message}`,
cause !== undefined ? { cause } : undefined,
);
this.statusCode = statusCode;
this.legacyCode = resolvedLegacyCode;
this.code = code;
this.fields = options?.fields;
}
}
// Registers all routes from a decorated controller instance to an Express router
export class ExtensionController {
logger?: Console;
// TODO DS: make this work with other express-like routers
registerRoutes () {
const logger = this.logger || console;
const prefix = Object.getPrototypeOf(this).__controllerPrefix || '';
const adminsForController = Object.getPrototypeOf(this).__adminUsernames as
| string[]
| undefined;
const allowedAppIdsForController = Object.getPrototypeOf(this).__allowedAppIds as
| string[]
| undefined;
const routes: RouteMeta[] = Object.getPrototypeOf(this).__routes || [];
for ( const route of routes ) {
const fullPath = `${prefix}/${route.path}`.replace(/\/+/g, '/');
const adminsForRoute = route.adminUsernames
? adminsForController
? adminsForController.concat(route.adminUsernames)
: route.adminUsernames
: adminsForController
? adminsForController
: undefined;
const allowedAppIds = route.allowedAppIds
? allowedAppIdsForController
? allowedAppIdsForController.concat(route.allowedAppIds)
: route.allowedAppIds
: allowedAppIdsForController
? allowedAppIdsForController
: undefined;
if ( ! extension[route.method] ) {
throw new Error(`Unsupported HTTP method: ${route.method}`);
} else {
logger.log(`Registering route: [${route.method.toUpperCase()}] ${fullPath}`);
(extension[route.method] as RouterMethods[HttpMethod])(
fullPath,
route.options || {},
async (req, res, next) => {
try {
if ( adminsForRoute || allowedAppIds ) {
if ( ! req.actor ) {
throw new HttpError(StatusCodes.UNAUTHORIZED, 'Unauthenticated');
}
}
if ( adminsForRoute ) {
if ( ! adminsForRoute.includes(req.actor!.type.user.username) ) {
throw new HttpError(
StatusCodes.FORBIDDEN,
'Only admins may request this resource.',
);
}
}
if ( allowedAppIds ) {
if ( ( req.actor!.type?.app?.uid && !allowedAppIds.includes(req.actor!.type.app.uid) ) ) {
throw new HttpError(
StatusCodes.FORBIDDEN,
'This app may not request this resource.',
);
}
}
return await route.handler.bind(this)(req, res, next);
} catch ( error ) {
if ( error instanceof HttpError ) {
const payload: Record<string, unknown> = {
error: error.message,
};
if ( error.legacyCode ) {
payload.code = error.legacyCode;
}
if ( error.code ) {
if ( payload.code === undefined ) {
payload.code = error.code;
} else {
payload.errorCode = error.code;
}
}
if ( error.fields ) {
for ( const [key, value] of Object.entries(error.fields) ) {
if ( payload[key] !== undefined ) {
continue;
}
payload[key] = value;
}
}
res.status(error.statusCode).send(payload);
logger.warn('httpError:', error);
return;
}
if ( error instanceof Error ) {
res.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ error: error.message });
logger.error('Non-http error:', error);
return;
}
res.status(StatusCodes.INTERNAL_SERVER_ERROR).send({ error: 'An unknown error occurred' });
logger.error('An unknown error occurred:', error);
}
},
);
}
}
}
}
@@ -1,15 +0,0 @@
import { Controller, Delete, ExtensionController, Get, HttpError, Post, Put } from './ExtensionController.js';
extension.exports = {
ExtensionController,
Controller,
Get,
Put,
Post,
Delete,
HttpError,
};
export {
Controller, Delete, ExtensionController, Get, HttpError, Post, Put,
};
@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"sourceMap": true,
"noEmitOnError": true,
"noImplicitAny": false,
"allowJs": true,
"checkJs": false,
},
"include": [
"./**/*.ts",
"./**/*.d.ts"
],
"exclude": [
"**/*.test.ts",
"**/*.spec.ts",
"**/test/**",
"**/tests/**",
"node_modules",
"dist",
"*.js"
]
}
-2653
View File
File diff suppressed because it is too large. Load diff
-29
View File
@@ -1,29 +0,0 @@
{
"name": "@heyputer/fs",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"type": "module",
"scripts": {
"postinstall": "tsc --noCheck",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^24.9.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"@aws-sdk/client-s3": "^3.1021.0"
},
"dependencies": {
"@aws-sdk/s3-request-presigner": "^3.1021.0",
"body-parser": "^2.2.0",
"busboy": "^1.6.0",
"http-status-codes": "^2.3.0",
"stripe": "^20.1.2",
"uuid": "^13.0.0"
}
}
-2
View File
@@ -1,2 +0,0 @@
*.js
*.js.map
File diff suppressed because it is too large. Load diff
@@ -1,153 +0,0 @@
import type { FSEntryRepository } from '../repositories/FSEntryRepository.js';
import type {
FsRemoveNodeEventPayload,
FsRemoveNodeTarget,
OuterGuiItemEventPayload,
} from './types.js';
export class FSEntryCacheInvalidationEventHandler {
#fsEntryRepository: FSEntryRepository;
constructor (fsEntryRepository: FSEntryRepository) {
this.#fsEntryRepository = fsEntryRepository;
this.#registerHandlers();
}
#registerHandlers (): void {
extension.on('outer.gui.item.added', async (event: OuterGuiItemEventPayload) => {
await this.#runSafely(() => this.#handleOuterGuiItemEvent(event), 'outer.gui.item.added');
});
extension.on('outer.gui.item.updated', async (event: OuterGuiItemEventPayload) => {
await this.#runSafely(() => this.#handleOuterGuiItemEvent(event), 'outer.gui.item.updated');
});
extension.on('outer.gui.item.moved', async (event: OuterGuiItemEventPayload) => {
await this.#runSafely(() => this.#handleOuterGuiItemEvent(event), 'outer.gui.item.moved');
});
extension.on('fs.remove.node', async (event: FsRemoveNodeEventPayload) => {
await this.#runSafely(() => this.#handleRemoveNodeEvent(event), 'fs.remove.node');
});
}
async #runSafely (handler: () => Promise<void>, eventName: string): Promise<void> {
try {
await handler();
} catch ( error ) {
console.error(`prodfsv2 cache invalidation failed for ${eventName}`, error);
}
}
#toUserIds (value: unknown): number[] {
if ( ! Array.isArray(value) ) {
return [];
}
const userIds: number[] = [];
for ( const item of value ) {
const numeric = Number(item);
if ( Number.isInteger(numeric) && numeric > 0 ) {
userIds.push(numeric);
}
}
return userIds;
}
#toNonEmptyString (value: unknown): string | null {
if ( typeof value !== 'string' ) {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
#isUnrecognizedTargetKeyError (error: unknown): boolean {
if ( ! (error instanceof Error) ) {
return false;
}
return error.message.includes('unrecognize key for FSNodeContext.get:');
}
async #readTargetValue (target: FsRemoveNodeTarget, keys: string[]): Promise<unknown> {
if ( typeof target.get !== 'function' ) {
return undefined;
}
for ( const key of keys ) {
try {
return await target.get(key);
} catch ( error ) {
if ( this.#isUnrecognizedTargetKeyError(error) ) {
continue;
}
throw error;
}
}
return undefined;
}
#extractUidFromEntry (value: unknown): string | null {
if ( !value || typeof value !== 'object' ) {
return null;
}
const entry = value as { uid?: unknown; uuid?: unknown };
return this.#toNonEmptyString(entry.uid) ?? this.#toNonEmptyString(entry.uuid);
}
async #handleOuterGuiItemEvent (event: OuterGuiItemEventPayload): Promise<void> {
const userIds = this.#toUserIds(event?.user_id_list);
const response = event?.response ?? {};
const path = this.#toNonEmptyString(response.path);
const oldPath = this.#toNonEmptyString(response.old_path);
const uid =
this.#toNonEmptyString(response.uid)
?? this.#toNonEmptyString(response.uuid)
?? this.#toNonEmptyString(response.id);
const tasks: Promise<void>[] = [];
for ( const userId of userIds ) {
if ( path ) {
tasks.push(this.#fsEntryRepository.invalidateEntryCacheByPathForUser(userId, path));
}
if ( oldPath && oldPath !== path ) {
tasks.push(this.#fsEntryRepository.invalidateEntryCacheByPathForUser(userId, oldPath));
}
}
if ( uid ) {
tasks.push(this.#fsEntryRepository.invalidateEntryCacheByUuid(uid));
}
if ( tasks.length > 0 ) {
await Promise.all(tasks);
}
}
async #handleRemoveNodeEvent (event: FsRemoveNodeEventPayload): Promise<void> {
const target = event?.target;
if ( !target || typeof target.get !== 'function' ) {
return;
}
const userIdValue = await this.#readTargetValue(target, ['user_id']);
const pathValue = await this.#readTargetValue(target, ['path']);
const uidValue =
await this.#readTargetValue(target, ['uid', 'uuid'])
?? this.#extractUidFromEntry(await this.#readTargetValue(target, ['entry']));
const userId = Number(userIdValue);
const path = this.#toNonEmptyString(pathValue);
const uuid = this.#toNonEmptyString(uidValue);
const tasks: Promise<void>[] = [];
if ( Number.isInteger(userId) && userId > 0 && path ) {
tasks.push(this.#fsEntryRepository.invalidateEntryCacheByPathForUser(userId, path));
}
if ( uuid ) {
tasks.push(this.#fsEntryRepository.invalidateEntryCacheByUuid(uuid));
}
if ( tasks.length > 0 ) {
await Promise.all(tasks);
}
}
}
@@ -1,20 +0,0 @@
export interface OuterGuiItemEventResponse {
path?: string;
old_path?: string;
uid?: string;
uuid?: string;
id?: string;
}
export interface OuterGuiItemEventPayload {
user_id_list?: Array<number | string>;
response?: OuterGuiItemEventResponse;
}
export interface FsRemoveNodeTarget {
get?: (key: string) => Promise<unknown> | unknown;
}
export interface FsRemoveNodeEventPayload {
target?: FsRemoveNodeTarget;
}
-3
View File
@@ -1,3 +0,0 @@
/// <reference path="../../api.d.ts" />
export {};
-6
View File
@@ -1,6 +0,0 @@
import '@heyputer/backend/src/services/User.js';
declare module '../../packages/puter/src/backend/src/services/User.d.ts' {
export interface IUser {
stripe_customer_id?: string;
}
}
-20
View File
@@ -1,20 +0,0 @@
import type { DynamoKVStore } from '@heyputer/backend/src/services/DynamoKVStore/DynamoKVStore.js';
import { FSController } from './controllers/FSController.js';
import { FSEntryCacheInvalidationEventHandler } from './eventHandlers/FSEntryCacheInvalidationEventHandler.js';
import { FSEntryRepository } from './repositories/FSEntryRepository.js';
import { S3StorageProvider } from './repositories/S3FileStorageRepository.js';
import { FSEntryService } from './services/FSEntryService.js';
const databaseService = extension.import('service:database');
const { cache, s3ClientProvider } = extension.import('data');
const eventService = extension.import('service:event');
const kvStore = extension.import('service:puter-kvstore') as DynamoKVStore;
const filesystemDb = databaseService;
const fsEntryRepository = new FSEntryRepository(filesystemDb, cache, kvStore);
const s3StorageProvider = new S3StorageProvider(s3ClientProvider);
const fsEntryService = new FSEntryService(fsEntryRepository, s3StorageProvider);
const fsController = new FSController(fsEntryService, eventService);
(fsController as unknown as { registerRoutes: () => void }).registerRoutes();
new FSEntryCacheInvalidationEventHandler(fsEntryRepository);
File diff suppressed because it is too large. Load diff
@@ -1,383 +0,0 @@
import {
AbortMultipartUploadCommand,
CompleteMultipartUploadCommand,
CreateMultipartUploadCommand,
DeleteObjectCommand,
PutObjectCommand,
type S3Client,
UploadPartCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import type { s3ClientProvider } from '@heyputer/backend/src/clients/s3/s3ClientProvider.js';
import { Readable } from 'node:stream';
import type {
MultipartCompleteInput,
ServerUploadInput,
SignedMultipartPartUrlsInput,
SignedUploadInput,
SignedUploadPart,
SignedUploadResult,
} from './s3Types.js';
export class S3StorageProvider {
#s3ClientProvider: typeof s3ClientProvider;
constructor (s3Provider: typeof s3ClientProvider) {
this.#s3ClientProvider = s3Provider;
}
#getClientForRegion (region: string): S3Client {
return this.#s3ClientProvider.get(region);
}
getMaxSingleUploadSize (): number {
return this.#s3ClientProvider.maxSingleUploadSize;
}
getMultipartPartSize (): number {
return this.#s3ClientProvider.partSize;
}
#resolveMultipartPartSize (requestedPartSize?: number): number {
return Math.max(
this.getMaxSingleUploadSize(),
requestedPartSize ?? this.getMultipartPartSize(),
);
}
async createSignedUploadUrl (fileMetadata: SignedUploadInput, region: string): Promise<SignedUploadResult> {
const [result] = await this.batchCreateSignedUploadUrls([fileMetadata], region);
if ( ! result ) {
throw new Error('Failed to create signed upload url');
}
return result;
}
async batchCreateSignedUploadUrls (filesMetadata: SignedUploadInput[], region: string): Promise<SignedUploadResult[]> {
const client = this.#getClientForRegion(region);
const now = Date.now();
const settledResults = await Promise.allSettled(filesMetadata.map(async (fileMetadata) => {
const expiresInSeconds = Math.max(60, Math.min(60 * 60, fileMetadata.expiresInSeconds));
const expiresAt = now + expiresInSeconds * 1000;
const maxSingleUploadSize = this.getMaxSingleUploadSize();
const shouldUseSingleUpload = fileMetadata.uploadMode === 'single'
&& fileMetadata.size <= maxSingleUploadSize;
if ( shouldUseSingleUpload ) {
const command = new PutObjectCommand({
Bucket: fileMetadata.bucket,
Key: fileMetadata.objectKey,
ContentType: fileMetadata.contentType,
});
const url = await getSignedUrl(client, command, { expiresIn: expiresInSeconds });
return {
uploadMode: 'single' as const,
expiresAt,
url,
};
}
const multipartPartSize = this.#resolveMultipartPartSize(fileMetadata.multipartPartSize);
const multipartPartCount = Math.max(1, Math.ceil(fileMetadata.size / multipartPartSize));
let multipartUploadId: string | undefined;
try {
const multipartResult = await client.send(new CreateMultipartUploadCommand({
Bucket: fileMetadata.bucket,
Key: fileMetadata.objectKey,
ContentType: fileMetadata.contentType,
}));
if ( ! multipartResult.UploadId ) {
throw new Error('Failed to initialize multipart upload');
}
multipartUploadId = multipartResult.UploadId;
const partUrls = await this.createSignedMultipartPartUrls({
bucket: fileMetadata.bucket,
objectKey: fileMetadata.objectKey,
multipartUploadId,
partNumbers: Array.from({ length: multipartPartCount }, (_, index) => index + 1),
expiresInSeconds,
}, region);
return {
uploadMode: 'multipart' as const,
expiresAt,
multipartUploadId,
multipartPartSize,
multipartPartCount,
multipartPartUrls: partUrls,
};
} catch ( error ) {
if ( multipartUploadId ) {
try {
await this.abortMutipartUpload(
multipartUploadId,
region,
fileMetadata.bucket,
fileMetadata.objectKey,
);
} catch {
// Best effort cleanup for partially initialized multipart uploads.
}
}
throw error;
}
}));
const failedResults = settledResults.filter((result) => result.status === 'rejected');
if ( failedResults.length > 0 ) {
await Promise.allSettled(settledResults.map((result, index) => {
if ( result.status !== 'fulfilled' ) {
return Promise.resolve();
}
if ( result.value.uploadMode !== 'multipart' || !result.value.multipartUploadId ) {
return Promise.resolve();
}
const fileMetadata = filesMetadata[index];
if ( ! fileMetadata ) {
return Promise.resolve();
}
return this.abortMutipartUpload(
result.value.multipartUploadId,
region,
fileMetadata.bucket,
fileMetadata.objectKey,
);
}));
const firstFailure = failedResults[0]?.reason;
if ( firstFailure instanceof Error ) {
throw firstFailure;
}
throw new Error('Failed to create signed upload urls');
}
return settledResults.map((result) => {
if ( result.status !== 'fulfilled' ) {
throw new Error('Failed to create signed upload urls');
}
return result.value;
});
}
async createSignedMultipartPartUrls (
input: SignedMultipartPartUrlsInput,
region: string,
): Promise<SignedUploadPart[]> {
const client = this.#getClientForRegion(region);
const expiresInSeconds = Math.max(60, Math.min(60 * 60, input.expiresInSeconds));
return Promise.all(input.partNumbers.map(async (partNumber) => {
const command = new UploadPartCommand({
Bucket: input.bucket,
Key: input.objectKey,
UploadId: input.multipartUploadId,
PartNumber: partNumber,
});
const url = await getSignedUrl(client, command, {
expiresIn: expiresInSeconds,
});
return {
partNumber,
url,
};
}));
}
async completeMultipartUpload (input: MultipartCompleteInput, region: string): Promise<void> {
const client = this.#getClientForRegion(region);
await client.send(new CompleteMultipartUploadCommand({
Bucket: input.bucket,
Key: input.objectKey,
UploadId: input.multipartUploadId,
MultipartUpload: {
Parts: [...input.parts]
.sort((partA, partB) => partA.partNumber - partB.partNumber)
.map((part) => ({
PartNumber: part.partNumber,
ETag: part.etag,
})),
},
}));
}
async abortMutipartUpload (uploadId: string, region: string, bucket: string, objectKey: string): Promise<void> {
const client = this.#getClientForRegion(region);
await client.send(new AbortMultipartUploadCommand({
Bucket: bucket,
Key: objectKey,
UploadId: uploadId,
}));
}
async uploadFromServer (input: ServerUploadInput, region: string): Promise<void> {
const client = this.#getClientForRegion(region);
const maxSingleUploadSize = this.getMaxSingleUploadSize();
const resolvedContentLength = this.#resolveContentLength(input);
const shouldUseMultipart = input.body instanceof Readable
? resolvedContentLength === undefined || resolvedContentLength > maxSingleUploadSize
: resolvedContentLength !== undefined && resolvedContentLength > maxSingleUploadSize;
if ( ! shouldUseMultipart ) {
await client.send(new PutObjectCommand({
Bucket: input.bucket,
Key: input.objectKey,
ContentType: input.contentType,
Body: input.body,
...(input.contentLength !== undefined ? { ContentLength: input.contentLength } : {}),
}));
return;
}
await this.#uploadFromServerMultipart(input, region, this.#resolveMultipartPartSize());
}
async deleteObject (bucket: string, objectKey: string, region: string): Promise<void> {
const client = this.#getClientForRegion(region);
await client.send(new DeleteObjectCommand({
Bucket: bucket,
Key: objectKey,
}));
}
#resolveContentLength (input: ServerUploadInput): number | undefined {
if ( Number.isFinite(input.contentLength) && Number(input.contentLength) >= 0 ) {
return Number(input.contentLength);
}
if ( Number.isFinite(input.sizeHint) && Number(input.sizeHint) >= 0 ) {
return Number(input.sizeHint);
}
if ( Buffer.isBuffer(input.body) || input.body instanceof Uint8Array ) {
return input.body.byteLength;
}
if ( typeof input.body === 'string' ) {
return Buffer.byteLength(input.body);
}
return undefined;
}
#toBuffer (chunk: unknown): Buffer {
if ( Buffer.isBuffer(chunk) ) {
return chunk;
}
if ( chunk instanceof Uint8Array ) {
return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
}
if ( typeof chunk === 'string' ) {
return Buffer.from(chunk);
}
throw new Error('Unsupported chunk type for multipart upload');
}
async #uploadFromServerMultipart (
input: ServerUploadInput,
region: string,
partSize: number,
): Promise<void> {
const client = this.#getClientForRegion(region);
const createResult = await client.send(new CreateMultipartUploadCommand({
Bucket: input.bucket,
Key: input.objectKey,
ContentType: input.contentType,
}));
const uploadId = createResult.UploadId;
if ( ! uploadId ) {
throw new Error('Failed to initialize multipart upload');
}
const completedParts: Array<{ ETag: string; PartNumber: number; }> = [];
let partNumber = 1;
const uploadPart = async (partBody: Buffer) => {
const uploadPartResult = await client.send(new UploadPartCommand({
Bucket: input.bucket,
Key: input.objectKey,
UploadId: uploadId,
PartNumber: partNumber,
Body: partBody,
ContentLength: partBody.byteLength,
}));
if ( ! uploadPartResult.ETag ) {
throw new Error(`Multipart upload returned no ETag for part ${partNumber}`);
}
completedParts.push({
ETag: uploadPartResult.ETag,
PartNumber: partNumber,
});
partNumber++;
};
try {
if ( Buffer.isBuffer(input.body) || input.body instanceof Uint8Array ) {
const bufferBody = this.#toBuffer(input.body);
for ( let offset = 0; offset < bufferBody.byteLength; offset += partSize ) {
const partBody = bufferBody.subarray(offset, offset + partSize);
await uploadPart(partBody);
}
} else if ( typeof input.body === 'string' ) {
const bufferBody = Buffer.from(input.body);
for ( let offset = 0; offset < bufferBody.byteLength; offset += partSize ) {
const partBody = bufferBody.subarray(offset, offset + partSize);
await uploadPart(partBody);
}
} else if ( input.body instanceof Readable ) {
let pendingChunk: Buffer = Buffer.alloc(0);
for await ( const chunk of input.body ) {
const chunkBuffer = this.#toBuffer(chunk);
if ( chunkBuffer.byteLength === 0 ) {
continue;
}
pendingChunk = pendingChunk.byteLength === 0
? chunkBuffer
: Buffer.concat([pendingChunk, chunkBuffer]);
while ( pendingChunk.byteLength >= partSize ) {
const partBody = pendingChunk.subarray(0, partSize);
await uploadPart(partBody);
pendingChunk = pendingChunk.subarray(partSize);
}
}
if ( pendingChunk.byteLength > 0 ) {
await uploadPart(pendingChunk);
}
} else {
throw new Error('Unsupported body type for multipart upload');
}
if ( completedParts.length === 0 ) {
await client.send(new AbortMultipartUploadCommand({
Bucket: input.bucket,
Key: input.objectKey,
UploadId: uploadId,
}));
await client.send(new PutObjectCommand({
Bucket: input.bucket,
Key: input.objectKey,
ContentType: input.contentType,
Body: Buffer.alloc(0),
ContentLength: 0,
}));
return;
}
await client.send(new CompleteMultipartUploadCommand({
Bucket: input.bucket,
Key: input.objectKey,
UploadId: uploadId,
MultipartUpload: {
Parts: completedParts,
},
}));
} catch ( error ) {
await client.send(new AbortMultipartUploadCommand({
Bucket: input.bucket,
Key: input.objectKey,
UploadId: uploadId,
})).catch(() => undefined);
throw error;
}
}
}
@@ -1,55 +0,0 @@
import type { Readable } from 'node:stream';
export interface SignedUploadInput {
bucket: string;
objectKey: string;
size: number;
contentType: string;
uploadMode: 'single' | 'multipart';
expiresInSeconds: number;
multipartPartSize?: number;
}
export interface SignedUploadPart {
partNumber: number;
url: string;
}
export interface SignedUploadResult {
uploadMode: 'single' | 'multipart';
expiresAt: number;
url?: string;
multipartUploadId?: string;
multipartPartSize?: number;
multipartPartCount?: number;
multipartPartUrls?: SignedUploadPart[];
}
export interface MultipartCompletePart {
partNumber: number;
etag: string;
}
export interface MultipartCompleteInput {
bucket: string;
objectKey: string;
multipartUploadId: string;
parts: MultipartCompletePart[];
}
export interface SignedMultipartPartUrlsInput {
bucket: string;
objectKey: string;
multipartUploadId: string;
partNumbers: number[];
expiresInSeconds: number;
}
export interface ServerUploadInput {
bucket: string;
objectKey: string;
contentType: string;
body: Buffer | Uint8Array | string | Readable;
contentLength?: number;
sizeHint?: number;
}
-51
View File
@@ -1,51 +0,0 @@
import type { FSEntryCreateInput } from '../types/FSEntry.js';
export interface FSEntryRow {
id: number;
uuid: string;
bucket: string | null;
bucket_region: string | null;
public_token: string | null;
file_request_token: string | null;
is_shortcut: number | boolean;
shortcut_to: number | null;
user_id: number;
parent_id: number | null;
parent_uid: string | null;
associated_app_id: number | null;
is_dir: number | boolean;
layout: string | null;
sort_by: 'name' | 'modified' | 'type' | 'size' | null;
sort_order: 'asc' | 'desc' | null;
is_public: number | boolean | null;
thumbnail: string | null;
immutable: number | boolean;
name: string;
metadata: string | null;
modified: number;
created: number | null;
accessed: number | null;
size: number | null;
symlink_path: string | null;
is_symlink: number | boolean;
path: string;
}
export interface NormalizedEntryWrite {
index: number;
input: FSEntryCreateInput;
userId: number;
targetPath: string;
parentPath: string;
fileName: string;
metadataJson: string | null;
bucket: string | null;
bucketRegion: string | null;
size: number;
createPaths: boolean;
}
export interface ReadEntriesByPathsOptions {
useTryHardRead?: boolean;
skipCache?: boolean;
}
Loaded 100 of 1815 files, more files were not shown because too many files have changed in this diff. Show more