Various docs fixes and AGENTS.md update (#4211)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s

This commit is contained in:
Reynaldi Chernando authored and GitHub committed 2026-10-13 17:29:46 +07:00
1 parent b8bb6292d6
commit 948772165d
37 files changed
+390 -321

No files matched your search

+46 -1
View File
@@ -120,7 +120,7 @@ Every SDK change carries all five of these, or the PR is incomplete. [doc/contri
1. **Backward compatibility**, unless a maintainer explicitly signs off on a break ([rules](doc/contributing-apis.md#maintaining-an-existing-api)).
2. **Tests** in the cross-platform API suite, or the Playwright harness for desktop-rendered `puter.ui.*` ([steps](doc/contributing-apis.md#6-tests)).
3. **Docs** in [src/docs/src/](src/docs/src/) that match the implementation exactly ([steps](doc/contributing-apis.md#5-docs)).
3. **Docs** in [src/docs/src/](src/docs/src/) that match the implementation exactly ([steps](doc/contributing-apis.md#5-docs), [writing rules](#developer-docs)).
4. **Types**, per the rules below.
5. **Error handling**: `{ message, code }` with stable `snake_case` codes, as in [modules/kv/](src/puter-js/src/modules/kv/) ([steps](doc/contributing-apis.md#3-puterjs)).
@@ -146,6 +146,51 @@ Use `@typedef {Object}` + `@property` for any shape whose fields need documentin
---
## Developer docs
[src/docs/src/](src/docs/src/) is the published puter.js documentation: one reference page per method (`<Area>/<method>.md`, [steps](doc/contributing-apis.md#5-docs)) and recipes in [src/docs/src/recipes/](src/docs/src/recipes/).
### Page structure
The reference has two kinds of page:
- **API overview pages** (`<Area>.md`, such as [KV.md](src/docs/src/KV.md)) give a quick top-level picture of what the API is for and what it can do, with short examples and a list of its functions that links every function page in the area. Leave out the details. The first sentence of the intro says what the API is about. This is a judgment call too: when the API depends on a Puter concept the reader may not know, such as Teams, the first sentence can name it and the sentences after it explain it.
- **Function pages** (`<Area>/<method>.md`) hold the details: parameters, defaults, return values, errors, limits, and edge cases.
An overview page answers "what is this API and is it what I need?", and a function page answers "how exactly does this call behave?". A change to one function usually only touches that function's page, so a new `kv.list()` option goes in `KV/list.md` without a matching update to `KV.md`. Don't copy a function's options, limits, or edge cases onto the overview page for completeness. Where a detail goes is a judgment call, not a hard rule: update the overview when a change affects how someone would describe the API as a whole.
When you add a page, add it to [src/docs/src/sidebar.js](src/docs/src/sidebar.js) under its area, or it won't show up in the navigation. When you add a runnable example, add its HTML to `src/docs/src/playground/examples/<slug>.html`, register the slug in [src/docs/src/examples.js](src/docs/src/examples.js), and link the code block to it by putting the slug after the language with a semicolon (` ```html;kv-set `).
### Writing docs
These apply to reference pages and recipes alike.
- **Document the developer-facing side only.** The reader is building on top of puter.js, so cover what affects their code: signatures, defaults, return shapes, errors, limits, and behavior they can observe. Leave out internal workings (which service handles the call, storage layout, caching, retries) unless they change what the developer has to do. [rate-limits-and-quotas.md](src/docs/src/rate-limits-and-quotas.md) is the exception: it publishes every limit, including timeouts and caps that come from how the backend works.
- **Describe the current state.** Docs aren't a changelog. Write how the API behaves now, without "now", "no longer", "existing code is unaffected", or other wording that compares it to an older version.
- **Verify before you document.** Run the calls you describe against a running Puter instead of inferring behavior from the source.
- **Write plainly.** Use the literal words a developer would say out loud, with contractions and everyday words. No idioms or figurative verbs ("walk away from something shared with you" → "remove your own access", "nothing to page through" → "without pagination"). No formal register ("present only while more pages remain" → "includes a `cursor` if there are more pages to load"). Prefer dev terms such as atomic, concurrent, and insertion order.
- **State the rule, then its consequence, in short sentences.** No metaphor in place of a rule. Name a concrete action ("you can create any files and folders inside it") over an abstract permission ("yours to organize freely"). Cut a sentence that only repeats the next one.
- **Describe behavior directly.** No quoted inner-voice phrases. Write "`isNew` is `true` when the person did not have access before", not "this tells 'I just gave this to someone' from 'they already had it'".
- **Keep references easy to follow.** Name what a list or code block belongs to in the sentence directly above it. Name a thing once and keep that name. Introduce an actor as "a user" before giving them a role: "A user can create a team. The user who creates the team becomes its owner."
- **Sentence mechanics.** Never start a sentence with inline code: write "To store data, use the [`set()`](/KV/set/) method." Link every inline mention of an API to its page, every time. No em dashes, and no colons or semicolons mid-sentence; a colon that introduces a code block is fine.
- **Show the correct way.** No "don't" and no wrong-way-first examples, in recipes and playground examples alike. Mention a wrong approach only when developers already hit it in practice, and state how it fails as a fact: "`add()` reads a plain object as a path map, so the call rejects with …".
### Recipes
Recipes are tested, copy-pasteable patterns for a task, one layer above the per-method reference, often combining several APIs. Agents find them through the `## Recipes` section of `llms.txt`.
- **Files.** One `.md` per recipe in [src/docs/src/recipes/](src/docs/src/recipes/), with `title`, `description`, `tags`, and optional `order` front matter; they're picked up automatically. Tags must exist in [src/docs/src/recipe-tags.js](src/docs/src/recipe-tags.js) or the build fails. Quote a `description` that contains a colon followed by a space. Every top-level `.md` in that directory is built as a recipe, so keep other markdown out of it.
- **Title.** Short, Title Case, starts with a verb, and leaves out "to Your App" ("Add Team Support", "Deploy Workers Programmatically"). The slug is the title in kebab case, with no API-name prefix. In `llms.txt` an agent sees only the title and description before deciding to open a recipe, so word them to match the task the reader is trying to do.
- **Description.** Written for someone who has never heard of Puter. Start with "Learn how to", put the action first, and say what the reader's app gains: "Learn how to add file sharing to your app with Puter.js, so users can share files in their Puter account with other people." No API names, no mechanism, no list of example cases, and no "not just …" contrast.
- **Intro.** Explain the situation and why you'd do this, tied to something the reader already knows ("`puter.kv` is your database in Puter.js"). No "Use this when" bullet list.
- **Structure.** Get to the first working call as early as possible, then add validation, rate limits, and sign-in after it. Explain a Puter concept the first time it appears, such as what `me.puter` is or that `puter.workers.exec()` opens the Puter sign-in window. Split recipes by what the reader needs, not by technique, and give a family of recipes a shared tag.
- **Link to the reference for full option lists.** Mention only the options the recipe uses, then link to the function page for the rest ("All the sort options are listed on [`puter.fs.readdir()`](/FS/readdir/)."). A copied list of options, rules, or scopes goes stale when the reference changes.
- **Code.** Platform-neutral: just the code, with top-level `await`. No `<script>` or `<html>` wrapper, no IIFE.
- **Tradeoffs.** Justify a choice with one scenario the reader can picture right away, in about two short paragraphs, or fold each point into the section where it matters. The description states the benefit only.
- **Notes.** Leave out `## Notes` by default. Use it only to point to an architecturally different approach. A limit goes in the section it applies to; other facts belong on the linked reference page.
---
## GUI
[src/gui/](src/gui/) is the Puter desktop: deliberately plain JavaScript + jQuery with HTML-string templates. Don't introduce a UI framework or a new rendering pattern.
+1 -1
View File
@@ -32,7 +32,7 @@ An object containing the following properties:
- `signal` (AbortSignal) (Optional) - Abort the request, including while waiting for the next streamed chunk. Cancellation rejects with the signal reason (normally an `AbortError`).
- `stream` (Boolean) - A boolean indicating whether you want to stream the completion. Defaults to `false`.
- `max_tokens` (Number) - The maximum number of tokens to generate in the completion. By default, the specific model's maximum is used.
- `temperature` (Number) - A number between 0 and 2 indicating the randomness of the completion. Lower values make the output more focused and deterministic, while higher values make it more random. By default, the specific model's temperature is used. OpenAI and Azure Responses calls omit unsupported sampling options. GPT-5.6 and GPT-6 Sol/Luna only receive `temperature` and `top_p` when the reasoning effort sent upstream is `none`; GPT-6 Astra, GPT-6.1 Sol, and GPT-5.3 Codex omit them. The `message.output_text.logprobs` include option is also omitted when sampling is unsupported.
- `temperature` (Number) - A number between 0 and 2 indicating the randomness of the completion. Lower values make the output more focused and deterministic, while higher values make it more random. By default, the model's own temperature is used. Some OpenAI reasoning models ignore `temperature` and `top_p`. GPT-5.6 and GPT-6 Sol/Luna accept them only with `reasoning_effort: 'none'`, and GPT-6 Astra, GPT-6.1 Sol, and GPT-5.3 Codex always ignore them.
- `tools` (Array) (Optional) - Function definitions the AI can call. See [Function Calling](#function-calling) for details.
- `reasoning_effort` / `reasoning.effort` (String) (Optional) - Controls how much effort reasoning models spend thinking. Supported values: `none`, `minimal`, `low`, `medium`, `high`, and `xhigh`. Lower values give faster responses with less reasoning. OpenAI models and Meta's Muse Spark models only; Muse Spark always reasons, so `none` is ignored for it.
- `verbosity` / `text.verbosity` (String) (Optional) - Controls how long or short responses are. Supported values: `low`, `medium`, and `high`. Lower values give shorter responses. OpenAI models only.
+1 -1
View File
@@ -42,7 +42,7 @@ Every call has the same shape; only the model name changes which service reads t
| `mistral-ocr-latest` (aliases `mistral-ocr-4`, `mistral-ocr-4-1`) | Mistral | Mistral OCR 4.1, the default Mistral model. Markdown output. |
| `mistral-ocr-2512` (aliases `mistral-ocr-3`, `mistral-ocr-3-0`) | Mistral | Mistral OCR 3, at a lower per-page rate than OCR 4. |
`mistral-ocr-latest` is pinned to OCR 4.1 and moves to a newer model only when Puter adds it. Puter rejects the deprecated `mistral-ocr-2503` and `mistral-ocr-2505` and the retired `mistral-ocr-4-0` with `bad_request`. Per-page prices for each model are listed by the API at `GET /metering/allCosts`.
`mistral-ocr-latest` is pinned to OCR 4.1 and moves to a newer model only when Puter adds it. Puter rejects the deprecated `mistral-ocr-2503` and `mistral-ocr-2505` and the retired `mistral-ocr-4-0` with `bad_request`.
#### AWS Textract options
+1 -1
View File
@@ -4,7 +4,7 @@ description: Retrieve the available AI chat models (and providers) that Puter cu
platforms: [websites, apps, nodejs, workers]
---
Returns the AI chat/completion models that are currently available to your app. The list is pulled from the same source as the public `/puterai/chat/models/details` endpoint and includes pricing and capability metadata where available.
Returns the AI chat/completion models that are currently available to your app, including pricing and capability metadata where available.
## Syntax
+2 -2
View File
@@ -4,7 +4,7 @@ description: Transform an audio clip into a different voice using ElevenLabs spe
platforms: [websites, apps, nodejs, workers]
---
Convert an existing recording into another voice while preserving timing, pacing, and delivery. This helper wraps the ElevenLabs voice changer endpoint so you can swap voices locally, from remote URLs, or with in-memory blobs.
Convert an existing recording into another voice while preserving timing, pacing, and delivery. Uses ElevenLabs voices.
## Syntax
@@ -30,7 +30,7 @@ Audio to convert. Accepts:
Fine-tune the conversion:
- `audio` (String | File | Blob): Alternate way to provide the source input.
- `voice` (String): Target ElevenLabs voice ID. Defaults to the configured ElevenLabs voice (`21m00Tcm4TlvDq8ikWAM` if unset).
- `voice` (String): Target ElevenLabs voice ID. Defaults to `21m00Tcm4TlvDq8ikWAM`.
- `model` (String): Voice-changer model. Defaults to `eleven_multilingual_sts_v2`. You can also use `eleven_english_sts_v2` for English-only inputs.
- `output_format` (String): Desired output codec and bitrate, e.g. `mp3_44100_128`, `opus_48000_64`, or `pcm_48000`. Defaults to `mp3_44100_128`.
- `voice_settings` (Object|String): ElevenLabs voice settings payload (e.g. `{"stability":0.5,"similarity_boost":0.75}`).
+1 -1
View File
@@ -4,7 +4,7 @@ description: Transcribe audio into text using OpenAI or xAI speech-to-text model
platforms: [websites, apps, nodejs, workers]
---
Converts spoken audio into text, with speaker diarization on xAI. This helper wraps the Puter driver-backed transcription API (OpenAI and xAI) so you can work with local files, remote URLs, or in-memory blobs from the browser.
Converts spoken audio into text, with speaker diarization on xAI. Supports OpenAI and xAI models.
## Syntax
+16 -16
View File
@@ -35,13 +35,13 @@ Additional settings for the generation request. Available options depend on the
| `model` | `String` | Image model ID. See the provider defaults and model discovery below. Lookups are case-insensitive; provider-prefixed IDs select that provider. |
| `test_mode` | `Boolean` | When `true`, returns a sample image without using credits |
| `puter_output_path` | `String` | When set, the generated image is automatically saved to this path on the Puter filesystem. Relative paths are resolved against the app's data directory (or `~/` outside an app). The caller must have write permission to the destination |
| `input_images` | `Array<String>` | Input image(s) for image-to-image — the canonical, cross-provider field (see below). |
| `input_images` | `Array<String>` | Input image(s) for image-to-image. Works with every provider (see below). |
| `input_image` | `String` | Single-image shorthand for `input_images`. |
| `input_image_mime_type` | `String` | MIME type of the input image(s), e.g. `'image/png'`. Used as a fallback when the type cannot be detected from the input — pass it when supplying raw base64 without a data-URI prefix. |
| `input_image_mime_type` | `String` | MIME type of the input image(s), e.g. `'image/png'`. Used as a fallback when the type cannot be detected from the input. Pass it when supplying raw base64 without a data-URI prefix. |
#### Providers and model discovery
When `model` is supplied, Puter selects its provider. Use a provider-prefixed ID such as `workers-ai:leonardo/lucid-origin` or `replicate:leonardoai/lucid-origin` to choose between providers offering similar models. A `provider` or legacy `driver` hint prefers that provider when it offers the model; otherwise a known model selects its own provider, preserving existing callers. A hint selecting a route excluded by data policy fails with `bad_request` instead of falling back. The generic `ai-image` hint is case-insensitive. The legacy `driver` option accepts full driver IDs such as `xai-image-generation`; `service` does not select an image provider.
When `model` is supplied, Puter selects its provider. Use a provider-prefixed ID such as `workers-ai:leonardo/lucid-origin` or `replicate:leonardoai/lucid-origin` to choose between providers offering similar models. A `provider` or legacy `driver` hint prefers that provider when it offers the model. Otherwise, a known model selects its own provider. A hint selecting a route excluded by data policy fails with `bad_request` instead of falling back. The generic `ai-image` hint is case-insensitive. The legacy `driver` option accepts full driver IDs such as `xai-image-generation`; `service` does not select an image provider.
Without a model, these provider defaults apply:
@@ -54,7 +54,7 @@ Without a model, these provider defaults apply:
| Replicate | `black-forest-labs/flux-schnell` |
| BytePlus | `seedream-5-0-lite-260128` |
When neither is supplied, the first configured provider with an available default model is used, with OpenAI preferred. Self-hosted instances expose only configured providers.
When neither is supplied, OpenAI's `gpt-image-2` is used.
Discover the image catalog, including model IDs, aliases, provider names, and pricing metadata:
@@ -67,19 +67,19 @@ console.table(models.map(({ id, provider }) => ({ id, provider })));
See the [AI model directory](https://developer.puter.com/ai/models/) for the included models and price units.
`puter.ai.listModels()` lists chat models. The image catalog above describes configured integrations; upstream account access and availability can still vary.
The [`puter.ai.listModels()`](/AI/listModels/) function lists chat models, not image models.
#### Output dimensions
Use `ratio: { w: 16, h: 9 }`, `aspect_ratio: '16:9'`, or a pair of pixel dimensions (`width`, `height`). `ratio` takes precedence, followed by `width`/`height`, then `aspect_ratio`. `aspect_ratio` always denotes a shape. Legacy dimension pairs with both sides at most 32 also denote shapes; BytePlus retains its pixel-threshold rule described below. Invalid, non-positive, or non-finite dimensions fail with `bad_request`.
Use `ratio: { w: 16, h: 9 }`, `aspect_ratio: '16:9'`, or a pair of pixel dimensions (`width`, `height`). `ratio` takes precedence, followed by `width`/`height`, then `aspect_ratio`. `aspect_ratio` always denotes a shape. Legacy dimension pairs with both sides at most 32 also denote shapes. BytePlus retains its pixel-threshold rule described below. Invalid, non-positive, or non-finite dimensions fail with `bad_request`.
Supported shapes and sizes vary by model. Gemini and BytePlus choose the closest supported aspect ratio; OpenAI scales any aspect hint into its pixel budget. Cloudflare expands aspect hints to about one megapixel, then scales or clamps dimensions to the per-side limits below. Cloudflare Schnell always generates 1024×1024 images.
Supported shapes and sizes vary by model. Gemini and BytePlus choose the closest supported aspect ratio. OpenAI scales any aspect hint into its pixel budget. Cloudflare expands aspect hints to about one megapixel, then scales or clamps dimensions to the per-side limits below. Cloudflare Schnell always generates 1024×1024 images.
#### Input images (image-to-image)
`input_images` is the universal way to pass image-to-image inputs across providers; `input_image` is the single-image shorthand. Each entry may be a **public URL**, a **data-URI**, or **raw base64** — providers that need base64 fetch URLs server-side (SSRF-guarded), so a URL works everywhere.
To pass input images for image-to-image, use `input_images`, which works with every provider. For a single image, `input_image` is a shorthand. Each entry can be a **public URL**, a **data-URI**, or **raw base64**, and a URL works with every provider.
Raw base64 carries no MIME type of its own. When it cannot be detected from the bytes, set `input_image_mime_type` (e.g. `'image/png'`) — Gemini rejects the request otherwise, and OpenAI, xAI, Cloudflare, BytePlus, and Replicate use it to label the upload.
Raw base64 carries no MIME type of its own. When it cannot be detected from the bytes, set `input_image_mime_type`, such as `'image/png'`. Gemini rejects the request without it, and OpenAI, xAI, Cloudflare, BytePlus, and Replicate use it to label the upload.
| Provider | Multiple images? | Accepted input forms |
|----------|------------------|----------------------|
@@ -99,8 +99,8 @@ Available when `provider: 'openai'` or inferred from model (`gpt-image-2.5-sunbu
| `model` | `String` | Image model to use. Available: `'gpt-image-2.5-sunburst'`, `'gpt-image-2.5-flare'`, `'gpt-image-2'` (default). |
| `quality` | `String` | Image quality: `'high'`, `'medium'`, `'low'` (default: `'low'`); `gpt-image-2` also accepts `'auto'`, and the `gpt-image-2.5-*` models also accept `'xhigh'`, `'max'`, and `'auto'`. Case-insensitive; unrecognized values fall back to `'low'`. |
| `ratio` | `Object` | Aspect ratio with `w` and `h` properties. All GPT Image models accept dynamic sizes subject to provider limits (16-pixel steps, 3:1 maximum ratio, 3840-pixel edge cap) |
| `input_image` | `String` | An input image for image-to-image editing — a URL or base64/data-URI (URLs are fetched server-side). |
| `input_images` | `Array<String>` | Multiple input images (URL or base64/data-URI) for image-to-image editing. Routes the request through OpenAI's image edit endpoint. |
| `input_image` | `String` | An input image for image-to-image editing, as a URL or base64/data-URI. |
| `input_images` | `Array<String>` | Multiple input images (URL or base64/data-URI) for image-to-image editing. |
For more details, see the [OpenAI API reference](https://platform.openai.com/docs/api-reference/images/create).
@@ -113,7 +113,7 @@ Available when `provider: 'gemini'` or inferred from model:
| `model` | `String` | `'gemini-3.1-flash-image'` (default), `'gemini-nano-banana-2.1'` (Nano Banana 2.1), `'gemini-3.1-flash-lite-image'`, or `'gemini-3-pro-image'`. |
| `ratio` | `Object` | Aspect ratio as `{ w, h }` (e.g., `{ w: 16, h: 9 }`). |
| `quality` | `String` | Nano Banana 2.1 and 3 Pro: `'1K'`, `'2K'`, `'4K'`; 3.1 Flash: `'512'`, `'1K'`, `'2K'`, `'4K'`; 3.1 Flash Lite: `'1K'`. Case-insensitive; a tier the model does not offer returns `bad_request` before any credits are checked. Defaults to the first supported tier: `'512'` on 3.1 Flash, `'1K'` on Nano Banana 2.1, 3 Pro, and 3.1 Flash Lite. |
| `input_images` | `Array<String>` | Input images for image-to-image — a URL or base64/data-URI (URLs are fetched server-side). |
| `input_images` | `Array<String>` | Input images for image-to-image, as URLs or base64/data-URIs. |
#### xAI (Grok) Options
@@ -126,7 +126,7 @@ Available when `provider: 'xai'` or inferred from model (`grok-imagine-image`, a
| `resolution` | `String` | Output resolution: `'1k'` (default) or `'2k'`. |
| `quality` | `String` | On 2.0 (case-insensitive): `'low'`, `'medium'`, or `'auto'` (default; low for generation, medium for edits). On older models, `'1k'`/`'2k'` remains a resolution alias. Explicit `resolution` takes precedence. |
| `input_image` | `String` | A public URL or base64-encoded (data-URI) input image for image-to-image editing. |
| `input_images` | `Array<String>` | Up to 5 input images (URLs or base64/data-URI) for multi-image editing — combine subjects, transfer styles, compose scenes. Routes through xAI's image edit endpoint. |
| `input_images` | `Array<String>` | Up to 5 input images (URLs or base64/data-URI) for multi-image editing, such as combining subjects, transferring styles, or composing scenes. |
#### Together Options
@@ -171,7 +171,7 @@ Available when `provider: 'replicate'` or inferred from model:
|--------|------|-------------|
| `model` | `String` | Model id (e.g. `'black-forest-labs/flux-schnell'`, `'leonardoai/lucid-origin'`). |
| `ratio` | `Object` | Aspect ratio as `{ w, h }` (e.g., `{ w: 16, h: 9 }`). |
| `input_image` | `String` | Input image for image-to-image generation — a URL or base64/data-URI. |
| `input_image` | `String` | Input image for image-to-image generation, as a URL or base64/data-URI. |
| `input_images` | `Array<String>` | Up to 10 input images (URL or base64/data-URI) for multi-image generation; more return `bad_request` before anything is fetched. Each fetched reference is capped at 30 MB; a larger one fails with `bad_request` before the request is sent. |
##### Per-model options
@@ -208,12 +208,12 @@ The original Replicate entries also support these options:
| `safety_tolerance` | `Number` | `flux-2-pro`, `flux-1.1-pro` | Safety tolerance level. |
| `prompt_upsampling` | `Boolean` | `flux-1.1-pro` | Enable prompt upsampling. |
| `response_format` | `String` | most models | Output format (e.g. `'webp'`, `'jpg'`, `'png'`). |
| `generation_mode` | `String` | Leonardo (`lucid-origin`) | Generation tier — affects pricing. `'standard'`/`'ultra'` (lucid-origin). |
| `generation_mode` | `String` | Leonardo (`lucid-origin`) | Generation tier, which affects pricing. `'standard'`/`'ultra'` (lucid-origin). |
| `style` | `String` | Leonardo | Stylistic preset. |
| `contrast` | `String` | Leonardo | Contrast preset. |
| `prompt_enhance` | `Boolean` | Leonardo | Server-side prompt enhancement. |
If a Replicate request is aborted, Puter keeps the creation response long enough to obtain the prediction ID and request cancellation. A generation that completes before cancellation is still metered. Predictions have a 10-minute [upstream deadline](https://replicate.com/docs/topics/predictions/create-a-prediction#prediction-deadlines). After the initial blocking request, Puter polls every 2 seconds and stops at the deadline with `upstream_timeout` (504). Cancellation cleanup polls for at most 30 seconds, plus any in-flight network request (30-second timeout). Each creation request has a separate 90-second network timeout and is sent once: a creation that fails on the network is not retried, so a call starts at most one prediction. A polling failure while the prediction is running surfaces as `upstream_failed` (502) without cancelling or metering it.
If you abort a Replicate request, Puter cancels the prediction. A generation that completes before it's cancelled is still metered. Predictions have a 10-minute [upstream deadline](https://replicate.com/docs/topics/predictions/create-a-prediction#prediction-deadlines), after which the call fails with `upstream_timeout` (504). A call starts at most one prediction, since a failed creation request isn't retried. If checking on a running prediction fails, the call fails with `upstream_failed` (502), and the prediction isn't cancelled or metered.
For more details, see the [Replicate API reference](https://replicate.com/docs) and each model's schema page on Replicate.
+2 -2
View File
@@ -28,7 +28,7 @@ Test mode still resolves the `model` you asked for and still validates (and writ
#### `options` (Object) (optional)
Additional settings for the generation request. Puter copies this object before resolving aliases and output paths; frozen options are supported. The options below carry the same meaning on every provider; each provider then accepts a few extras, listed in the sections that follow. Any option a provider does not recognize is ignored.
Additional settings for the generation request.
| Option | Type | Description |
|--------|------|-------------|
@@ -51,7 +51,7 @@ Additional settings for the generation request. Puter copies this object before
Model ids are matched case-insensitively, and every model answers to a few spellings: the fully qualified id, the `org/model` form and the bare model name. `'togetherai:wan-ai/wan2.7-t2v'`, `'wan-ai/wan2.7-t2v'` and `'wan2.7-t2v'` all select the same model. When a bare name is served by more than one provider Puter picks the cheaper listing; pass `provider` or the fully qualified id to choose explicitly.
When you pass no model, Puter uses Wan 2.7 text-to-video on Together AI (`wan2.7-t2v`). A self-hosted Puter without a Together AI key falls back to the first provider it has a key for.
When you pass no model, Puter uses Wan 2.7 text-to-video on Together AI (`wan2.7-t2v`).
Google's Veo 3.1 preview models (`veo-3.1-generate-preview`, `veo-3.1-fast-generate-preview`, `veo-3.1-lite-generate-preview`) are no longer offered: Google deprecated them with a shutdown on October 22, 2026, and requests for those ids fail with `Model not found`. Veo 3.1 and Veo 3.1 Lite remain available through Together AI (see below).
+1 -1
View File
@@ -43,7 +43,7 @@ When the request includes `cursor` (even `null`), `offset`, or `includeTotal`, t
- `cursor` (String) (optional): Present while more pages exist; pass it to the next call.
- `total` (Number) (optional): Total app count, present when `includeTotal` was set.
Requests without pagination params keep returning the full list as a plain array, so existing code is unaffected — under the hood the SDK now fetches it page by page.
Without pagination params, the full list is returned as a plain array.
With `stream: true`, the method returns an async iterator of page objects instead:
+1 -1
View File
@@ -27,7 +27,7 @@ When signed out with no `username`, it resolves to `null` without opening a sign
## Example
```html;auth-get-profile
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
+1 -1
View File
@@ -29,7 +29,7 @@ When signed out with no `username`, it resolves to `null` without opening a sign
## Example
```html;auth-get-profile-picture
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
+2 -2
View File
@@ -6,9 +6,9 @@ platforms: [websites, apps, nodejs, workers]
The Email API lets your app read the user's Puter mailbox and send transactional email.
Every Puter account has an email address, `{username}@puter.email`. Mail sent to it is stored in the user's own cloud drive, so your app can list the inbox and read messages, attachments included, once the user grants access to their mailbox. Your app can also send transactional email — a signup confirmation, a receipt, an alert — from a Puter-controlled address, with no mail server, sending domain, or DKIM to set up. A recipient with a Puter account can be reached at `{username}@puter.email`: that copy is filed straight into their Puter mailbox rather than relayed, on any plan, as long as they have set their mailbox up.
Every Puter account has an email address, `{username}@puter.email`. Mail sent to it is stored in the user's own cloud drive, so your app can list the inbox and read messages, attachments included, once the user grants access to their mailbox. Your app can also send transactional email, such as a signup confirmation, a receipt, or an alert, from a Puter address without setting up a mail server.
With the [User-Pays Model](/user-pays-model/), the account making the call covers its own usage: mail lives in the user's storage, and sends land on the caller's allowance rather than yours.
With the [User-Pays Model](/user-pays-model/), the account making the call covers its own usage. Mail is stored in the user's storage, and sends count toward the caller's allowance rather than yours.
<div class="info">Transactional email is available to accounts on a paid plan for now. A call from a free account fails with <code>402</code> <code>subscription_required</code>.</div>
+2 -1
View File
@@ -22,7 +22,7 @@ What you watch is written as a subject string:
| `kv:cart` | One key in your app's key-value store (`kv:cart*` for a prefix) |
| `notif:account` | The user's notifications |
See [`onLocal()`](/Events/onLocal/#subjects) for the full subject syntax, the event shape, and how to share key-value events with another user.
See [Subjects and events](/Events/subjects/) for the full subject syntax, the event shape, and how to share key-value events with another user.
With the [User-Pays Model](/user-pays-model/), deliveries are billed to the user holding the subscription, not to you. See [Rate Limits and Quotas](/rate-limits-and-quotas/#events) for limits and costs.
@@ -270,6 +270,7 @@ These Events features are supported out of the box when using Puter.js:
- **[`puter.events.fetch()`](/Events/fetch/)** - Read what a subject recorded while nothing was listening
- **[`puter.events.handlers`](/Events/handlers/)** - Publish, list and remove the named handlers persistent subscriptions run
- **[`puter.events.workers`](/Events/workers/)** - List and destroy an app's events worker
- **[Subjects and events](/Events/subjects/)** - The subject syntax and the shape of every event
## Examples
+1 -1
View File
@@ -104,7 +104,7 @@ An app's first published handler creates its [events worker](/Events/workers/),
### Refusing a delivery
In the events worker, a handler that returns takes the delivery, and one that throws is retried later. When retrying can't help (a malformed event, say), throw an error with `terminal: true` or `code: 'events_terminal'`. The delivery is dropped and replaced by a [gap marker](/Events/onLocal/#gaps) with `reason: 'handler_rejected'`.
In the events worker, a handler that returns takes the delivery, and one that throws is retried later. When retrying can't help (a malformed event, say), throw an error with `terminal: true` or `code: 'events_terminal'`. The delivery is dropped and replaced by a [gap marker](/Events/subjects/#gaps) with `reason: 'handler_rejected'`.
```js
await puter.events.handlers.publish('ingestUpload', async ({ event }) => {
+6 -208
View File
@@ -6,7 +6,7 @@ platforms: [websites, apps, nodejs]
<div class="info">The Events API is in beta. Event shapes, limits, and behavior may change between releases.</div>
Subscribes to a subject and calls `handler` each time something matching it changes. The subscription belongs to this client's connection: nothing is stored, nothing runs while the page is closed, and it ends when the connection does. Changes from other devices and regions reach it too; one made in another region typically arrives a few hundred milliseconds later than a local one. See [Events](/Events/) for the subject grammar and the event shape.
Subscribes to a subject and calls `handler` each time something matching it changes. The subscription belongs to this client's connection: nothing is stored, nothing runs while the page is closed, and it ends when the connection does. Changes from other devices and regions reach it too; one made in another region typically arrives a few hundred milliseconds later than a local one. See [Subjects and events](/Events/subjects/) for the subject syntax and the event shape.
Not for Puter workers: a subscription there only lasts for one invocation. To react to changes from a worker, use [`onPersistent()`](/Events/onPersistent/) with a published handler.
@@ -21,16 +21,16 @@ puter.events.onLocal(subject, handler, options)
## Parameters
#### `subject` (String) (required)
What to watch: `fs:<path or uid>[:<op>]`, `kv:<key>`, or `notif:<audience>`. See [Subjects](#subjects).
What to watch: `fs:<path or uid>[:<op>]`, `kv:<key>`, or `notif:<audience>`. See [Subjects](/Events/subjects/).
#### `handler` (Function) (required)
Called with `{ event }` for each delivery; see [The event](#the-event). `event.op === 'gap'` means events were dropped by a limit or missed while the connection was down: re-read what you're watching (see [Gaps](#gaps)). A handler that throws is logged to the console; the subscription continues.
Called with `{ event }` for each delivery; see [The event](/Events/subjects/#the-event). `event.op === 'gap'` means events were dropped by a limit or missed while the connection was down: re-read what you're watching (see [Gaps](/Events/subjects/#gaps)). A handler that throws is logged to the console; the subscription continues.
#### `options` (Object) (optional)
- `onError` (Function): Called with `{ message, code }` if the subscription can't be restored after a disconnect: re-subscribing failed, the reconnect was refused, or the server kept closing the connection. A single disconnect is reconnected without calling it; the handler gets a `reconnect` gap instead. After `onError`, the subscription is over; call `onLocal()` again to resume. It's also called with `code: 'subscription_ended'` and a `reason` when the server ends the subscription (see [Subscriptions the server ends](#subscriptions-the-server-ends)). Without it, the error is logged to the console.
- `timeout` (Number): How long to wait for the server to confirm the subscription, in milliseconds. Defaults to `30000`.
- `includeValue` (Boolean): For a `kv:` subject, include the key's new value as `event.value` (`null` on a `del`, absent on an `expire`). See [Getting the new value](#getting-the-new-value) for when it's left out. Refused on other subjects.
- `includeValue` (Boolean): For a `kv:` subject, include the key's new value as `event.value` (`null` on a `del`, absent on an `expire`). See [Getting the new value](/Events/subjects/#getting-the-new-value) for when it's left out. Refused on other subjects.
## Return value
@@ -66,215 +66,13 @@ The promise rejects with `{ message, code }`:
| `events_connection_failed` | The connection couldn't be opened, the server didn't answer in time, or it kept closing the connection. |
| `events_failed` | The server sent a response the SDK couldn't read. |
## Subjects
```
fs:<path or uid>[:<op>]
kv:<key>
kv:<appId>:<key>
notif:<audience>
notif:<appId>:<audience>
```
The same subjects work with [`onPersistent()`](/Events/onPersistent/).
### Files
- **Path**: absolute (`/alice/Documents`), home-relative (`~/Documents`), or relative (`inbox` or `./inbox`). A relative path resolves the same way [`puter.fs`](/FS/) paths do: under your app's `AppData` folder in an app, under home otherwise. A directory covers everything under it, at any depth.
- **Uid**: the `uid` of a file or directory, to follow one node wherever it moves. A relative name shaped like a uid is read as one; write `./<name>` to mean the path.
- **Op**: one of `add`, `write`, `move`, `remove`, `meta`. Leave it off to get all of them. Nothing emits `meta` yet.
```js
await puter.events.onLocal('fs:~/Documents', handler); // everything under Documents
await puter.events.onLocal('fs:inbox', handler); // inbox in your app's folder
await puter.events.onLocal('fs:~/Documents/notes.txt:write', handler); // one file, writes only
await puter.events.onLocal('fs:~/Pictures/*.png', handler); // wildcard within one segment
await puter.events.onLocal('fs:~/Projects/**/build.log', handler); // across directories
```
`*` matches within one path segment, `**` crosses directories, and `?` matches one character. A subject may use one `*` per segment and one `**` in total; more is rejected with `invalid_subject_pattern`.
#### Watching a path before it exists
A subject can name a path that isn't there. The subscription anchors on the nearest existing directory and matches the rest of the path, so it behaves as if the path already existed: you get the event when it appears and, for a folder, every change inside it afterwards. A pattern still matches only the paths it names.
```js
// Nothing at this path yet. The handler runs when it's created.
await puter.events.onLocal('fs:~/Documents/inbox/trigger.json:add', ({ event }) => {
process(event.path);
});
```
If the node a subscription is attached to is deleted, a subscription made with a uid, or with a path that existed when you subscribed, ends: `onError` is called with `subscription_ended` and `reason: 'anchor_deleted'`. Call `onLocal()` again to watch the path once it's back. One made with a pattern, or with a path that didn't exist yet, moves up to the nearest folder that still exists and that you can read, and keeps watching, so recreating the path resumes delivery. If there's no such folder (for example, the owner of a folder shared with you deleted it), or the pattern would grow past its limits, it ends the same way.
### Notifications
`notif:` watches part of the account's notification mailbox:
- `notif:account`: notifications about the account itself, such as files shared with it. Only a client acting as the account, not as an app, sees them. Apps and websites never do: subscribing from one is refused with `subject_does_not_exist`.
- `notif:app-user`: notifications belonging to the app you're running as.
- `notif:developer`: notifications about an app you own.
The two-segment form is expanded to your app's id for you. Notifications are stored, so you can also read missed ones with [`fetch()`](/Events/fetch/).
### Key-value data
A `kv:` subject watches your app's key-value store:
```js
await puter.events.onLocal('kv:cart', ({ event }) => refresh(event.key)); // exactly the key `cart`
await puter.events.onLocal('kv:cart*', handler); // every key starting with `cart`
```
> **Exact by default; add `*` to widen.** `kv:cart` matches only `cart`, and `kv:cart*` matches every key starting with `cart`. This is the opposite of [`puter.kv.list()`](/KV/list/), which always matches a prefix.
Only a trailing `*` is allowed. A `*` anywhere else, or a `?`, is rejected with `invalid_kv_pattern`.
The segment after `kv:` is read as an app id when there are more segments after it, so a key that contains `:` needs the full three-part form:
```js
await puter.events.onLocal('kv:orders:pending', handler); // app `orders`, key `pending`
await puter.events.onLocal(`kv:${puter.appID}:orders:pending`, handler); // your app, key `orders:pending`
```
The returned `anchor` names the app whose store is watched; `subject` stays as you passed it.
#### Getting the new value
A delivery names the key, not its value. Pass `includeValue: true` to receive the new value as `event.value` (`null` on a `del`, absent on an `expire`):
```js
await puter.events.onLocal('kv:cart', ({ event }) => render(event.value), { includeValue: true });
```
The value is left out, and you read the key yourself, when:
- it's over **16 KB** serialized, or
- more than **128** subscriptions match the change in that region (including ones that didn't ask for values), or the filter-check limit stops the count early.
#### Data from other apps
Watching another app's key-value data takes the same consent as reading it: the app must allow data sharing, and the user must grant your app `app-data:<appId>:kv:read`. Both are checked when you subscribe and on every delivery. Where cross-app watching is off, the subject is refused with `events_cross_app_disabled`.
Entries the other app wrote with `disableSharing` are never delivered. Marking a key private produces no event, so a value you already have for it may be stale.
## Sharing key-value events with another user
`kv:` always means your own data. To let another account watch part of yours, mint a **share handle** over a key prefix and give it to them. They subscribe with the handle where an app id would go:
```js
// The owner shares one workspace with another account.
const res = await fetch(`${puter.APIOrigin}/events/kv-handles`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${puter.authToken}`,
},
body: JSON.stringify({
granteeUsername: 'bob',
// Use a segment you'll never rename: the handle is fixed to this prefix.
prefix: `workspace:${workspaceId}:`,
}),
});
const { handle } = await res.json();
```
```js
// Bob watches every key written in that workspace.
// `event.key` is relative to the handle: `messages:1`, not `workspace:<id>:messages:1`.
await puter.events.onLocal(`kv:${handle}:*`, ({ event }) => render(event.key));
```
- **The holder learns only the handle**: not whose data it is, where it sits, or anything above the prefix. `subject` and `key` in their deliveries are relative to the handle. `kv:<handle>:messages:*` narrows to part of the shared region.
- **Values:** `includeValue` works through a handle, with the same limits. A handle grants events, not reads, so when a value is left out the holder needs some other authorized way to read it.
- **Private entries stay private.** An entry written with `disableSharing` is never delivered through a handle: no event, key, or value.
- **Use a stable prefix.** A handle stays on the prefix it was minted on. Rename `workspace:<uuid>:` to `project:<uuid>:` and existing handles point at keys nothing writes. Prefer synthetic ids (`workspace:<uuid>:`, `thread:<uuid>:`) over names that might change (`acme-corp:`).
- **Prefixes are literal.** `*`, `?` and empty segments (`workspace::abc:`) are refused with `invalid_kv_share_prefix`. The trailing `:` is optional.
- **Keys can't leave the handle.** A handle with no key after it, or a key using `..`, is refused with `invalid_kv_handle_key`.
**Managing handles.** `GET /events/kv-handles` lists the handles the account has minted, revoked ones included. `DELETE /events/kv-handles/<handle>` revokes one, and every persistent subscription on it is suspended with `permission_revoked` and its backlog dropped. Revoking an already-revoked handle isn't an error. See [Rate Limits and Quotas](/rate-limits-and-quotas/#events) for how many handles an account can hold. Temporary accounts can't mint handles (`events_kv_handle_requires_account`). Where the feature is off, minting and handle subjects fail with `events_kv_handles_disabled`.
**Apps.** An app can mint, list and revoke handles for its user, but only inside its own namespace and with the user's consent: `manage:kv-share:<userUuid>:<appId>:<prefix>`, requested with [`puter.perms.request()`](/Perms/request/). The prefix supplies its segments, so `workspace:abc:` ends the permission as `…:workspace:abc`. The consent must name a prefix; asking for the whole namespace is refused with `invalid_kv_share_prefix`. An app that lists handles sees only its own namespace.
| Refused with | When |
| --- | --- |
| `events_kv_handle_not_delegated` | Minting or revoking outside the consented prefix, or after the consent was withdrawn (the handle stays in place). |
| `events_kv_handle_outside_namespace` | Minting outside the app's own namespace. |
| `events_kv_handle_owner_only` | The caller is an access token the app issued, a different app of the same user, or the handle is outside the app's namespace. |
An app can also subscribe through a handle, but only when the shared region belongs to that same app. A different app, even for the same user, is refused as if the handle didn't exist.
## Access
Subscribing takes the same access as reading. A subject you can't read and a subject that doesn't exist both fail with `subject_does_not_exist`, so the error doesn't reveal which. Access is checked again on every delivery: when a share is revoked, deliveries stop immediately. The subscription itself stays open, so `onError` isn't called — not even if the node is later deleted.
## The event
The handler is called with `{ event }`. A filesystem change carries:
| Field | Type | Description |
| --- | --- | --- |
| `id` | String | Unique id for the event. |
| `subject` | String | The node it happened to, as `fs:<uid>:<op>`. Not the subject you subscribed with. |
| `op` | String | `add` (a create, or a copy's destination), `write`, `move` (a move or a rename), or `remove`. |
| `uid` | String | The uid of the node that changed. |
| `path` | String | The path of the node that changed. |
| `from` | String | On a `move`, the path the node left. Only present when the subscription was watching that location. |
| `self` | Boolean | `true` when the account holding the subscription made the change. Use it to ignore your own writes. |
| `ts` | Number | When it happened, in milliseconds since the epoch. |
| `seq` | Number | Position within one dispatch, when a change goes to several subscriptions. |
A key-value change carries `key` instead of `uid` and `path`, and different ops:
| Field | Type | Description |
| --- | --- | --- |
| `id` | String | Unique id for the event. |
| `subject` | String | `kv:<appId>:<key>`, naming the key that changed. |
| `op` | String | `set` for a write, `del` for a removal, `expire` when only the key's lifetime changed. |
| `key` | String | The key that changed. |
| `value` | Any | The new value (`null` on a `del`). Only with `includeValue`, and only within the [limits above](#getting-the-new-value). Never on an `expire`. |
| `self` | Boolean | As above. |
| `ts` | Number | As above. |
| `seq` | Number | As above. |
A notification carries the fields listed on [`fetch()`](/Events/fetch/#return-value).
Events never say *who* made a change: on a shared folder, that would tell every subscriber who else has access.
[`puter.kv.flush()`](/KV/flush/) delivers nothing, since the keys a flush can list aren't reliably the keys it removed.
Writes to the same subject within 250 ms arrive as one event carrying the latest state, so a multipart upload or a save loop is one delivery rather than one per write.
### Gaps
Limits never make a subscription fail. Instead they send a **gap marker** in place of what was dropped: an event with `op: 'gap'`, a `reason`, and no `uid` or `path`. Treat it as "re-read what I'm watching", never as "nothing changed". A reconnect sends one too.
| `reason` | Cause |
| --- | --- |
| `matched_subscription_limit` | The event matched more subscriptions than one event may reach. |
| `filter_evaluation_limit` | The event had more subscription filters to check than one event may check. |
| `delivery_rate_limit` | This subscription went over its per-minute delivery rate. |
| `backlog_overflow` | A persistent subscription's undelivered backlog was full, so the oldest deliveries were dropped. |
| `handler_rejected` | A persistent subscription's handler refused the delivery (a `4xx`, or a thrown terminal error). |
| `suspended_backlog_expired` | A persistent subscription stayed [suspended](/Events/onPersistent/#suspended-subscriptions) past the time its backlog is kept. |
| `reconnect` | The connection dropped and came back. Changes made while it was down weren't delivered. Sent once, after the subscription is restored. |
```js
await puter.events.onLocal('fs:~/Documents', async ({ event }) => {
if (event.op === 'gap') return refreshEverything();
apply(event);
});
```
See [Rate Limits and Quotas](/rate-limits-and-quotas/#events) for the numbers behind each limit.
## Reconnects
All of a client's session subscriptions share one connection, opened by the first `onLocal()` and closed when the last one ends.
If the connection drops (a network blip, a sign-in, an API origin change, or the server closing it), the SDK reconnects and subscribes again. The handler and subscription object stay the same; only `subId` changes, so don't store anything against it. `onError` is called only if the subscription can't be restored: re-subscribing fails, the reconnect is refused (`reauth_required` after a sign-out), or the server keeps closing the connection (`events_connection_failed`).
Changes made while it was down aren't sent afterwards. Once the subscription is back, the handler gets one [gap marker](#gaps) with `reason: 'reconnect'`, so a handler that re-reads on gaps catches up by itself. For `notif:` subjects, [`fetch()`](/Events/fetch/) reads exactly what was missed.
Changes made while it was down aren't sent afterwards. Once the subscription is back, the handler gets one [gap marker](/Events/subjects/#gaps) with `reason: 'reconnect'`, so a handler that re-reads on gaps catches up by itself. For `notif:` subjects, [`fetch()`](/Events/fetch/) reads exactly what was missed.
```js
const sub = await puter.events.onLocal('fs:~/Documents', handler, {
@@ -288,7 +86,7 @@ The server can end a session subscription on its own, without a disconnect. `onE
| `code` | `reason` | When |
| --- | --- | --- |
| `subscription_ended` | `anchor_deleted` | The node it was attached to was deleted, and it couldn't move up to a parent folder (see [Watching a path before it exists](#watching-a-path-before-it-exists)). |
| `subscription_ended` | `anchor_deleted` | The node it was attached to was deleted, and it couldn't move up to a parent folder (see [Watching a path before it exists](/Events/subjects/#watching-a-path-before-it-exists)). |
If the connection is down when this happens, or drops before `onError` is called, the SDK isn't told. It subscribes again with the original subject when it reconnects: a path then anchors on its nearest existing folder and keeps watching, like a path that doesn't exist yet, while a uid fails and `onError` is called with `subject_does_not_exist` instead.
+2 -2
View File
@@ -10,7 +10,7 @@ Creates a subscription that outlives this connection. It's stored on the account
The subscription is live immediately in the region where it was created. Changes made in other regions can take a moment longer (usually well under a second) to reach it right after this call resolves.
See [`onLocal()`](/Events/onLocal/#subjects) for the subject grammar and the event shape.
See [Subjects and events](/Events/subjects/) for the subject syntax and the event shape.
On a website with nobody signed in, it asks the user to sign in first, as other Puter.js calls do. An app running on Puter is always signed in.
@@ -59,7 +59,7 @@ Whichever place runs it, it's the same code, called with:
| Binding | What it is |
| --- | --- |
| `event` | The [event](/Events/onLocal/#the-event), or a [gap marker](/Events/onLocal/#gaps). |
| `event` | The [event](/Events/subjects/#the-event), or a [gap marker](/Events/subjects/#gaps). |
| `ctx` | The frozen `context` this subscription was created with. |
| `user` | A `puter` bound to the account holding the subscription, with the same access your app has for that user in a tab. In a client, it's the ambient `puter`. |
| `fetch` | [`puter.net.fetch`](/Networking/fetch/) where it exists, otherwise the environment's `fetch`. |
+209
View File
@@ -0,0 +1,209 @@
---
title: Subjects and events
description: The subject strings Events subscriptions watch, and the events they deliver.
platforms: [websites, apps, nodejs, workers]
---
<div class="info">The Events API is in beta. Event shapes, limits, and behavior may change between releases.</div>
A subject is the string that says what a subscription watches. [`onLocal()`](/Events/onLocal/) and [`onPersistent()`](/Events/onPersistent/) take the same subjects and deliver the same events.
## Syntax
```
fs:<path or uid>[:<op>]
kv:<key>
kv:<appId>:<key>
notif:<audience>
notif:<appId>:<audience>
```
## Files
- **Path**: absolute (`/alice/Documents`), home-relative (`~/Documents`), or relative (`inbox` or `./inbox`). A relative path resolves the same way [`puter.fs`](/FS/) paths do: under your app's `AppData` folder in an app, under home otherwise. A directory covers everything under it, at any depth.
- **Uid**: the `uid` of a file or directory, to follow one node wherever it moves. A relative name shaped like a uid is read as one; write `./<name>` to mean the path.
- **Op**: one of `add`, `write`, `move`, `remove`, `meta`. Leave it off to get all of them. Nothing emits `meta` yet.
```js
await puter.events.onLocal('fs:~/Documents', handler); // everything under Documents
await puter.events.onLocal('fs:inbox', handler); // inbox in your app's folder
await puter.events.onLocal('fs:~/Documents/notes.txt:write', handler); // one file, writes only
await puter.events.onLocal('fs:~/Pictures/*.png', handler); // wildcard within one segment
await puter.events.onLocal('fs:~/Projects/**/build.log', handler); // across directories
```
`*` matches within one path segment, `**` crosses directories, and `?` matches one character. A subject may use one `*` per segment and one `**` in total; more is rejected with `invalid_subject_pattern`.
### Watching a path before it exists
A subject can name a path that isn't there. The subscription anchors on the nearest existing directory and matches the rest of the path, so it behaves as if the path already existed: you get the event when it appears and, for a folder, every change inside it afterwards. A pattern still matches only the paths it names.
```js
// Nothing at this path yet. The handler runs when it's created.
await puter.events.onLocal('fs:~/Documents/inbox/trigger.json:add', ({ event }) => {
process(event.path);
});
```
If the node a subscription is attached to is deleted, a subscription made with a uid, or with a path that existed when you subscribed, ends with `reason: 'anchor_deleted'`. Subscribe again to watch the path once it's back. [`onLocal()`](/Events/onLocal/#subscriptions-the-server-ends) reports this through `onError`, and a persistent subscription reports it with an `app.events.ended` notification you can read with [`fetch()`](/Events/fetch/). One made with a pattern, or with a path that didn't exist yet, moves up to the nearest folder that still exists and that you can read, and keeps watching, so recreating the path resumes delivery. If there's no such folder (for example, the owner of a folder shared with you deleted it), or the pattern would grow past its limits, it ends the same way.
## Notifications
`notif:` watches part of the account's notification mailbox:
- `notif:account`: notifications about the account itself, such as files shared with it. Only a client acting as the account, not as an app, sees them. Apps and websites never do: subscribing from one is refused with `subject_does_not_exist`.
- `notif:app-user`: notifications belonging to the app you're running as.
- `notif:developer`: notifications about an app you own.
The two-segment form is expanded to your app's id for you. Notifications are stored, so you can also read missed ones with [`fetch()`](/Events/fetch/).
## Key-value data
A `kv:` subject watches your app's key-value store:
```js
await puter.events.onLocal('kv:cart', ({ event }) => refresh(event.key)); // exactly the key `cart`
await puter.events.onLocal('kv:cart*', handler); // every key starting with `cart`
```
> **Exact by default; add `*` to widen.** `kv:cart` matches only `cart`, and `kv:cart*` matches every key starting with `cart`. This is the opposite of [`puter.kv.list()`](/KV/list/), which always matches a prefix.
Only a trailing `*` is allowed. A `*` anywhere else, or a `?`, is rejected with `invalid_kv_pattern`.
The segment after `kv:` is read as an app id when there are more segments after it, so a key that contains `:` needs the full three-part form:
```js
await puter.events.onLocal('kv:orders:pending', handler); // app `orders`, key `pending`
await puter.events.onLocal(`kv:${puter.appID}:orders:pending`, handler); // your app, key `orders:pending`
```
The returned `anchor` names the app whose store is watched; `subject` stays as you passed it.
### Getting the new value
A delivery names the key, not its value. Pass `includeValue: true` to receive the new value as `event.value` (`null` on a `del`, absent on an `expire`):
```js
await puter.events.onLocal('kv:cart', ({ event }) => render(event.value), { includeValue: true });
```
The value is left out, and you read the key yourself, when:
- it's over **16 KB** serialized, or
- more than **128** subscriptions match the change in that region (including ones that didn't ask for values), or the filter-check limit stops the count early.
### Data from other apps
Watching another app's key-value data takes the same consent as reading it: the app must allow data sharing, and the user must grant your app `app-data:<appId>:kv:read`. Both are checked when you subscribe and on every delivery. Where cross-app watching is off, the subject is refused with `events_cross_app_disabled`.
Entries the other app wrote with `disableSharing` are never delivered. Marking a key private produces no event, so a value you already have for it may be stale.
## Sharing key-value events with another user
`kv:` always means your own data. To let another account watch part of yours, mint a **share handle** over a key prefix and give it to them. They subscribe with the handle where an app id would go:
```js
// The owner shares one workspace with another account.
const res = await fetch(`${puter.APIOrigin}/events/kv-handles`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${puter.authToken}`,
},
body: JSON.stringify({
granteeUsername: 'bob',
// Use a segment you'll never rename: the handle is fixed to this prefix.
prefix: `workspace:${workspaceId}:`,
}),
});
const { handle } = await res.json();
```
```js
// Bob watches every key written in that workspace.
// `event.key` is relative to the handle: `messages:1`, not `workspace:<id>:messages:1`.
await puter.events.onLocal(`kv:${handle}:*`, ({ event }) => render(event.key));
```
- **The holder learns only the handle**: not whose data it is, where it sits, or anything above the prefix. `subject` and `key` in their deliveries are relative to the handle. `kv:<handle>:messages:*` narrows to part of the shared region.
- **Values:** `includeValue` works through a handle, with the same limits. A handle grants events, not reads, so when a value is left out the holder needs some other authorized way to read it.
- **Private entries stay private.** An entry written with `disableSharing` is never delivered through a handle: no event, key, or value.
- **Use a stable prefix.** A handle stays on the prefix it was minted on. Rename `workspace:<uuid>:` to `project:<uuid>:` and existing handles point at keys nothing writes. Prefer synthetic ids (`workspace:<uuid>:`, `thread:<uuid>:`) over names that might change (`acme-corp:`).
- **Prefixes are literal.** `*`, `?` and empty segments (`workspace::abc:`) are refused with `invalid_kv_share_prefix`. The trailing `:` is optional.
- **Keys can't leave the handle.** A handle with no key after it, or a key using `..`, is refused with `invalid_kv_handle_key`.
**Managing handles.** `GET /events/kv-handles` lists the handles the account has minted, revoked ones included. `DELETE /events/kv-handles/<handle>` revokes one, and every persistent subscription on it is suspended with `permission_revoked` and its backlog dropped. Revoking an already-revoked handle isn't an error. See [Rate Limits and Quotas](/rate-limits-and-quotas/#events) for how many handles an account can hold. Temporary accounts can't mint handles (`events_kv_handle_requires_account`). Where the feature is off, minting and handle subjects fail with `events_kv_handles_disabled`.
**Apps.** An app can mint, list and revoke handles for its user, but only inside its own namespace and with the user's consent: `manage:kv-share:<userUuid>:<appId>:<prefix>`, requested with [`puter.perms.request()`](/Perms/request/). The prefix supplies its segments, so `workspace:abc:` ends the permission as `…:workspace:abc`. The consent must name a prefix; asking for the whole namespace is refused with `invalid_kv_share_prefix`. An app that lists handles sees only its own namespace.
| Refused with | When |
| --- | --- |
| `events_kv_handle_not_delegated` | Minting or revoking outside the consented prefix, or after the consent was withdrawn (the handle stays in place). |
| `events_kv_handle_outside_namespace` | Minting outside the app's own namespace. |
| `events_kv_handle_owner_only` | The caller is an access token the app issued, a different app of the same user, or the handle is outside the app's namespace. |
An app can also subscribe through a handle, but only when the shared region belongs to that same app. A different app, even for the same user, is refused as if the handle didn't exist.
## Access
Subscribing takes the same access as reading. A subject you can't read and a subject that doesn't exist both fail with `subject_does_not_exist`, so the error doesn't reveal which. Access is checked again on every delivery: when a share is revoked, deliveries stop immediately. The subscription itself stays open, even if the node is later deleted.
## The event
The handler is called with `{ event }`. A filesystem change carries:
| Field | Type | Description |
| --- | --- | --- |
| `id` | String | Unique id for the event. |
| `subject` | String | The node it happened to, as `fs:<uid>:<op>`. Not the subject you subscribed with. |
| `op` | String | `add` (a create, or a copy's destination), `write`, `move` (a move or a rename), or `remove`. |
| `uid` | String | The uid of the node that changed. |
| `path` | String | The path of the node that changed. |
| `from` | String | On a `move`, the path the node left. Only present when the subscription was watching that location. |
| `self` | Boolean | `true` when the account holding the subscription made the change. Use it to ignore your own writes. |
| `ts` | Number | When it happened, in milliseconds since the epoch. |
| `seq` | Number | Position within one dispatch, when a change goes to several subscriptions. |
A key-value change carries `key` instead of `uid` and `path`, and different ops:
| Field | Type | Description |
| --- | --- | --- |
| `id` | String | Unique id for the event. |
| `subject` | String | `kv:<appId>:<key>`, naming the key that changed. |
| `op` | String | `set` for a write, `del` for a removal, `expire` when only the key's lifetime changed. |
| `key` | String | The key that changed. |
| `value` | Any | The new value (`null` on a `del`). Only with `includeValue`, and only within the [limits above](#getting-the-new-value). Never on an `expire`. |
| `self` | Boolean | As above. |
| `ts` | Number | As above. |
| `seq` | Number | As above. |
A notification carries the fields listed on [`fetch()`](/Events/fetch/#return-value).
Events never say *who* made a change: on a shared folder, that would tell every subscriber who else has access.
[`puter.kv.flush()`](/KV/flush/) delivers nothing, since the keys a flush can list aren't reliably the keys it removed.
Writes to the same subject within 250 ms arrive as one event carrying the latest state, so a multipart upload or a save loop is one delivery rather than one per write.
### Gaps
Limits never make a subscription fail. Instead they send a **gap marker** in place of what was dropped: an event with `op: 'gap'`, a `reason`, and no `uid` or `path`. Treat it as "re-read what I'm watching", never as "nothing changed". A reconnect sends one too.
| `reason` | Cause |
| --- | --- |
| `matched_subscription_limit` | The event matched more subscriptions than one event may reach. |
| `filter_evaluation_limit` | The event had more subscription filters to check than one event may check. |
| `delivery_rate_limit` | This subscription went over its per-minute delivery rate. |
| `backlog_overflow` | A persistent subscription's undelivered backlog was full, so the oldest deliveries were dropped. |
| `handler_rejected` | A persistent subscription's handler refused the delivery (a `4xx`, or a thrown terminal error). |
| `suspended_backlog_expired` | A persistent subscription stayed [suspended](/Events/onPersistent/#suspended-subscriptions) past the time its backlog is kept. |
| `reconnect` | The connection dropped and came back. Changes made while it was down weren't delivered. Sent once, after the subscription is restored. |
```js
await puter.events.onLocal('fs:~/Documents', async ({ event }) => {
if (event.op === 'gap') return refreshEverything();
apply(event);
});
```
See [Rate Limits and Quotas](/rate-limits-and-quotas/#events) for the numbers behind each limit.
+1 -2
View File
@@ -5,8 +5,6 @@ description: Store and manage data in the user's own cloud drive with Puter.js f
The Cloud Storage API lets you store and manage data in the cloud.
Local [uploads](/FS/upload/) can optionally generate browser image thumbnails or use a custom thumbnail callback. The callback can delegate to the built-in image generator and respond to upload cancellation. The Puter desktop additionally provides PDF previews without adding a PDF renderer to the SDK.
It comes with a comprehensive but familiar file system operations including write, read, delete, move, and copy for files, plus powerful directory management features like creating directories, listing contents, and much more.
With Puter.js, you don't need to worry about setting up storage infrastructure such as configuring buckets, managing CDNs, or ensuring availability, since everything is handled for you. Additionally, with the [User-Pays Model](/user-pays-model/), you don't have to worry about storage or bandwidth costs, as users of your application cover their own usage.
@@ -306,6 +304,7 @@ These cloud storage features are supported out of the box when using Puter.js:
- **[`puter.fs.copy()`](/FS/copy/)** - Copy a file or directory
- **[`puter.fs.move()`](/FS/move/)** - Move a file or directory
- **[`puter.fs.stat()`](/FS/stat/)** - Get information about a file or directory
- **[`puter.fs.space()`](/FS/space/)** - Get the user's storage capacity and usage
- **[`puter.fs.delete()`](/FS/delete/)** - Delete a file or directory
- **[`puter.fs.upload()`](/FS/upload/)** - Upload a file from the local system
- **[`puter.fs.getReadURL()`](/FS/getReadURL/)** - Generate a URL that can be used to read a file
+1 -1
View File
@@ -45,7 +45,7 @@ Items you own appear at their real path. An item you shared as a delegate (from
<strong class="example-title">See everything you have shared</strong>
```html;fs-listSharedByMe
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
+1 -1
View File
@@ -49,7 +49,7 @@ When the request includes `cursor` (even `null`) or `includeTotal`, or is `recur
- `cursor` (String) (optional): Present while more pages exist; pass it to the next call.
- `total` (Number) (optional): Total entry count, present when `includeTotal` was set.
Requests without pagination params keep returning the full listing as a plain array, so existing code is unaffected — under the hood the SDK now fetches it page by page.
Without pagination params, the full listing is returned as a plain array.
With `stream: true`, the method returns an async iterator of page objects instead:
+16 -13
View File
@@ -1,40 +1,43 @@
---
title: puter.fs.space()
description: Check how much storage capacity the user has and how much they have used.
platforms: [websites, apps, nodejs, workers]
---
Returns the storage space capacity and usage for the current user.
<div class="info">
<svg style="margin-right:15px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48" stroke-width="2"><g stroke-width="2" transform="translate(0, 0)"><circle data-color="color-2" data-stroke="none" cx="24" cy="35" r="1" fill="#ffffff"></circle><circle cx="24" cy="24" r="22" fill="none" stroke="#fff" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></circle><line data-color="color-2" x1="24" y1="12" x2="24" y2="28" fill="none" stroke="#ffffff" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></line><circle data-color="color-2" cx="24" cy="35" r="1" fill="none" stroke="#ffffff" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2" stroke-linejoin="miter"></circle></g></svg>
This method requires permission to access the user's storage space. If the user has not granted permission, the method will return an error.</div>
Returns the storage capacity and usage of the current user.
## Syntax
```js
puter.fs.space()
```
## Parameters
None.
## Return value
A `Promise` that will resolve to an object with the following properties:
- `capacity` (Number): The total amount of storage capacity available to the user, in bytes.
- `used` (Number): The amount of storage space used by the user, in bytes.
A `Promise` that resolves to a [`SpaceInfo`](/Objects/spaceinfo/) object with these properties:
- `capacity` (Number): The total storage capacity available to the user, in bytes.
- `used` (Number): The amount of storage the user has used, in bytes.
## Examples
<strong class="example-title">Show how much storage the user has used</strong>
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
// Retrieves the storage space capacity and usage for the current user, and prints them to the browser console
puter.space().then((space)=>{
console.log(space)
});
(async () => {
const { used, capacity } = await puter.fs.space();
const toMB = (bytes) => (bytes / 1024 / 1024).toFixed(1);
puter.print(`Used ${toMB(used)} MB of ${toMB(capacity)} MB`);
})();
</script>
</body>
</html>
```
```
+1 -1
View File
@@ -33,7 +33,7 @@ When the request includes `cursor` (even `null`) or `includeTotal`, the promise
- `cursor` (String) (optional): Present while more pages exist; pass it to the next call.
- `total` (Number) (optional): Present when `includeTotal` was set.
Requests without pagination params keep returning the full list as a plain array, so existing code is unaffected — under the hood the SDK now fetches it page by page.
Without pagination params, the full list is returned as a plain array.
With `stream: true`, the method returns an async iterator of page objects instead:
+2 -2
View File
@@ -11,8 +11,6 @@ Puter.js handles all the infrastructure for you, so you don't need to set up ser
<div class="info"><strong>Need to share data across users?</strong> Each user's key-value store lives in their own account, so one user can't read another's data. To keep a single, centralized store that every user reads from and writes to, use a <a href="/Workers/">Serverless Worker</a> — its code can act on the worker owner's resources, giving all users one shared backend.</div>
<div class="info"><strong>Key layout is the access boundary.</strong> To let another account watch part of your store instead of copying it, mint an <a href="/Events/onLocal/#sharing-key-value-events-with-another-user">Events share handle</a> over a key prefix. A handle pins the prefix it was granted on, so reorganizing your keys breaks every handle already given out — grant on a stable synthetic segment such as <code>workspace:&lt;uuid&gt;:</code> rather than a semantic one like <code>q3-planning:</code>, which is the kind of name that gets renamed.</div>
## Features
<div style="overflow:hidden; margin-bottom: 30px;">
@@ -225,6 +223,8 @@ These Key-Value Store features are supported out of the box when using Puter.js:
- **[`puter.kv.expireAt()`](/KV/expireAt/)** - Set key expiration timestamp
- **[`puter.kv.list()`](/KV/list/)** - List keys in ascending or descending order
- **[`puter.kv.flush()`](/KV/flush/)** - Clear all data
- **[`puter.kv.MAX_KEY_SIZE`](/KV/MAX_KEY_SIZE/)** - Maximum key size in bytes
- **[`puter.kv.MAX_VALUE_SIZE`](/KV/MAX_VALUE_SIZE/)** - Maximum value size in bytes
## Examples
+2 -2
View File
@@ -58,7 +58,7 @@ When paginating, iterate until the result has no `cursor` — a page may hold fe
Invalid options, such as a non-boolean `reverse` or `offset` combined with `stream`, reject the returned promise with an `invalid_request` error; `list()` never throws synchronously. With `stream: true`, the iterator's first `next()` rejects instead.
Full (non-paginated) listings keep resolving to a plain array, so existing code is unaffected — under the hood the SDK now fetches them page by page. They still read the entire store, though: every page is metered, so on large stores a bare `list()` gets slow and costly (the SDK logs a one-time console warning when a full listing spans multiple pages). Prefer `stream: true` or explicit `limit`/`cursor` pages, and narrow the scan with a `pattern`.
Without pagination params, the full listing is returned as a plain array. It reads the entire store and is metered per page, so on large stores a bare `list()` gets slow and costly. The SDK logs a one-time console warning when a full listing spans multiple pages. Use `stream: true` or `limit` and `cursor` pages instead, and narrow the scan with a `pattern`.
With `stream: true`, the method returns an async iterator of [`KVListPage`](/Objects/kvlistpage) objects instead:
@@ -74,7 +74,7 @@ for await (const page of puter.kv.list({ pattern: 'log:*', stream: true })) {
<strong class="example-title">List keys in reverse order</strong>
```html;kv-list-reverse
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
+1 -1
View File
@@ -38,7 +38,7 @@ A Unix timestamp in seconds at which the key should expire. Omit it, or pass `nu
#### `disableSharing` (Boolean) (optional)
Pass inside the trailing options object — `set(key, value, { disableSharing: true })` — to mark this entry private to your app. A private entry cannot be read, listed, changed, or deleted by any other app, even one the user has granted access to your app's data with [`puter.perms.request('appData', …)`](/Perms/appData/). It is also not watched by other apps, or by another user through a [share handle](/Events/onLocal/#sharing-key-value-events-with-another-user): those subscriptions get no event, key, or value for it. Use it for anything another app should never see, such as a cached access token: a user approving a request cannot see what your store holds.
Pass inside the trailing options object — `set(key, value, { disableSharing: true })` — to mark this entry private to your app. A private entry cannot be read, listed, changed, or deleted by any other app, even one the user has granted access to your app's data with [`puter.perms.request('appData', …)`](/Perms/appData/). It is also not watched by other apps, or by another user through a [share handle](/Events/subjects/#sharing-key-value-events-with-another-user): those subscriptions get no event, key, or value for it. Use it for anything another app should never see, such as a cached access token: a user approving a request cannot see what your store holds.
The batch form takes it too — `set([...items], { disableSharing: true })` marks every entry in the batch.
+1 -1
View File
@@ -59,7 +59,7 @@ A rejection carries an `Error` with a stable `code`:
<strong class="example-title">Update an element of a root array</strong>
```html;kv-update-root-array
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
+5
View File
@@ -11,6 +11,7 @@ Various object types and classes that represent different entities in the Puter
- **[AppConnection](/Objects/AppConnection/)** - Represents a connection to an application
- **[ChatResponse](/Objects/chatresponse/)** - Represents an AI chat response
- **[ChatResponseChunk](/Objects/chatresponsechunk/)** - Represents a chunk of streaming chat response data
- **[CreateAppResult](/Objects/createappresult/)** - Represents the result of creating an app
- **[DetailedAppUsage](/Objects/detailedappusage/)** - Represents detailed resource usage statistics for a specific application
- **[EmailListPage](/Objects/emaillistpage/)** - Represents one page of mailbox listing results
- **[EmailMessage](/Objects/emailmessage/)** - Represents a fully parsed message from the user's mailbox
@@ -18,9 +19,12 @@ Various object types and classes that represent different entities in the Puter
- **[EmailSummary](/Objects/emailsummary/)** - Represents one message in a mailbox listing
- **[FSItem](/Objects/fsitem/)** - Represents a file or directory
- **[KVPair](/Objects/kvpair/)** - Represents a key-value pair
- **[KVListPage](/Objects/kvlistpage/)** - Represents one page of key-value list results
- **[MonthlyUsage](/Objects/monthlyusage/)** - Represents user's monthly resource usage information
- **[PuterPeerConnection](/Objects/puterpeerconnection/)** - Represents a data-channel connection to a peer
- **[PuterPeerServer](/Objects/puterpeerserver/)** - Represents a peer server and its connected clients
- **[SignInResult](/Objects/signinresult/)** - Represents the result of a sign-in
- **[SpaceInfo](/Objects/spaceinfo/)** - Represents the user's storage capacity and usage
- **[Speech2TxtResult](/Objects/speech2txtresult/)** - Represents speech-to-text transcription results
- **[Subdomain](/Objects/subdomain/)** - Represents a subdomain
- **[Team](/Objects/team/)** - Represents a Puter team
@@ -29,5 +33,6 @@ Various object types and classes that represent different entities in the Puter
- **[TTSVoice](/Objects/ttsvoice/)** - Represents an available text-to-speech voice
- **[ToolCall](/Objects/toolcall/)** - Represents a tool invocation request
- **[User](/Objects/user/)** - Represents a Puter user
- **[UserProfile](/Objects/userprofile/)** - Represents a user's public profile
- **[WorkerDeployment](/Objects/workerdeployment/)** - Represents a worker deployment result
- **[WorkerInfo](/Objects/workerinfo/)** - Represents worker information
+1
View File
@@ -131,6 +131,7 @@ These peer features are supported out of the box when using Puter.js:
- **[`puter.peer.serve()`](/Peer/serve/)** - Create a peer server and generate an invite code
- **[`puter.peer.connect()`](/Peer/connect/)** - Connect to a peer server using an invite code
- **[`puter.peer.ensureTurnRelays()`](/Peer/ensureTurnRelays/)** - Preload TURN relays for faster connections
- **[`puter.peer.createGuestGrant()`](/Peer/createGuestGrant/)** - Create a grant that lets a guest without a Puter account use Puter's relays
## Examples
+1 -21
View File
@@ -6,9 +6,7 @@ platforms: [websites, apps]
The Permissions API enables your application to request access to user data and resources such as email addresses, special folders (Desktop, Documents, Pictures, Videos), apps, subdomains, and other apps' saved data.
There are two methods. [`puter.perms.request()`](/Perms/request/) asks the user for access, and [`puter.perms.check()`](/Perms/check/) reports whether they have already granted it without prompting.
Both take the same two arguments: the resource being asked about, and the details that resource needs.
Each request names a resource and the details that resource needs:
```js
await puter.perms.request('email');
@@ -17,8 +15,6 @@ await puter.perms.request('apps', { access: 'read' });
await puter.perms.request('appData', { app: 'contacts', scopes: 'read' });
```
When requesting permissions, users will be prompted to grant or deny access. If a permission has already been granted, the user will not be prompted again. This provides a seamless experience while maintaining user privacy and control.
## Features
<div style="overflow:hidden; margin-bottom: 30px;">
@@ -190,22 +186,6 @@ Pass an array to ask for everything your app needs in one call. Anything already
</div>
## Resources
The resource decides which details the call takes and what a request resolves to:
| Resource | Details | `request()` resolves to |
| -------- | ------- | ----------------------- |
| `'email'` | — | The user's email address |
| `'folder'` | `{ name, access }` | The folder's path |
| `'apps'` | `{ access }` | `true` if granted |
| `'subdomains'` | `{ access }` | `true` if granted |
| `'appData'` | `{ app, scopes }` | `true` if granted |
| `'appRootDir'` | `{ app, access }` | The app's root directory, or `undefined` if denied |
| `'permission'` | `{ permission }` or `{ permissions }` | `true` if granted |
Anything denied resolves to a falsy value, so one `if` covers both outcomes. `access` is `'read'` (the default) or `'write'`.
## Functions
- **[`puter.perms.request()`](/Perms/request/)** - Request access to a resource, or to several at once
+6 -4
View File
@@ -6,9 +6,13 @@ platforms: [websites, apps]
Request access to something belonging to the user. The first argument names the resource; the second carries the details that resource takes.
The user is prompted to allow or deny. Anything already granted is skipped, so a call whose access is fully in place doesn't prompt at all. [`puter.perms.check()`](/Perms/check/) reads the same state, so the two always agree.
<div class="info">
Inside the Puter desktop the prompt is shown as a dialog. On websites, it opens in a popup window on the Puter origin — call this from a user gesture (e.g. a click handler) so the browser doesn't block the popup; without a gesture, a consent dialog is shown first and the popup opens when the user clicks Continue.
The user is prompted to grant or deny access. If they already granted it, they aren't prompted again, so a call whose access is fully in place doesn't prompt at all. [`puter.perms.check()`](/Perms/check/) reads the same state, so the two always agree.
</div>
Inside the Puter desktop the prompt is shown as a dialog. On websites, it opens in a popup window on the Puter origin. Call this from a user gesture, such as a click handler, so the browser doesn't block the popup. Without a gesture, a consent dialog is shown first and the popup opens when the user clicks Continue.
## Syntax
@@ -66,8 +70,6 @@ The array form resolves to an array of those values, in the order asked.
Some permission strings are not supported and are denied silently.
An app's root directory has no raw form: `app-root-dir:` reads as nothing during a permission check, so asking for it this way would prompt on every call and [`check()`](/Perms/check/) would report it as not granted even once it is. Use the `'appRootDir'` resource, which asks the server directly.
A lone string that names no resource is treated as a permission string, so `puter.perms.request('fs:/user/Documents:read')` keeps working.
## Creating a path on request
+1 -1
View File
@@ -4,7 +4,7 @@ description: Retrieves the current language/locale code from the Puter environme
platforms: [apps]
---
Retrieves the current language/locale code from the Puter environment. This function communicates with the host environment to get the active language setting.
Retrieves the current language/locale code from the Puter environment.
## Syntax
```js
+1 -1
View File
@@ -35,7 +35,7 @@ When the request includes any pagination option, the promise instead resolves to
- `cursor` (String) (optional): Present while more pages exist; pass it to the next call.
- `total` (Number) (optional): Present when `includeTotal` was set.
Requests without pagination params keep returning the full list as a plain array, so existing code is unaffected — under the hood the SDK now fetches it page by page.
Without pagination params, the full list is returned as a plain array.
With `stream: true`, the method returns an async iterator of page objects instead:
+2 -2
View File
@@ -361,7 +361,7 @@ The mutation and read budgets are per user, per app, not per team: administering
### Events
See [Events](/Events/) for an overview, [`onLocal()`](/Events/onLocal/#gaps) for gap markers, and [`onPersistent()`](/Events/onPersistent/#suspended-subscriptions) for suspension.
See [Events](/Events/) for an overview, [`onLocal()`](/Events/subjects/#gaps) for gap markers, and [`onPersistent()`](/Events/onPersistent/#suspended-subscriptions) for suspension.
#### What an account can hold
@@ -389,7 +389,7 @@ Counted per event, per region:
- KV limits follow the plan of the **key's owner**, not the writer's or the subscribers'. If the owner's plan can't be looked up, the free numbers apply. A server with no metering uses the paid numbers.
- They count subscriptions, not people. A user with three tabs open counts three times.
- Past the limit, the next subscriptions get a [gap marker](/Events/onLocal/#gaps) instead of the event (up to the same number again), and any after that get nothing. With 1,100 subscriptions on a paid owner's key: 512 get the event, 512 get a gap, 76 get nothing.
- Past the limit, the next subscriptions get a [gap marker](/Events/subjects/#gaps) instead of the event (up to the same number again), and any after that get nothing. With 1,100 subscriptions on a paid owner's key: 512 get the event, 512 get a gap, 76 get nothing.
- The region that handles the write delivers to every persistent subscription plus its own connected clients. Other regions deliver only to their own connected clients. So persistent subscriptions share one budget per event, while session subscriptions spread across regions can reach more in total.
- `includeValue` values are left out of every delivery for a change when more than **128** subscriptions match it (whatever the owner's plan), when the filter-check limit stops the count early, or when the value is over **16 KB**.
@@ -33,8 +33,8 @@ const deployment = await puter.workers.create('notes-api', 'notes-api.js');
deployment.url; // 'https://notes-api.puter.work'
```
Give it 5 to 30 seconds before the first request, while the worker spreads to
every edge server. Then call it with
Give it 5 to 30 seconds before the first request, while the worker deploys.
Then call it with
[`puter.workers.exec()`](/Workers/exec/), which sends the signed-in user along
so the route can use `user.puter`:
@@ -46,11 +46,10 @@ await res.json(); // { username: 'grace' }
[Build an API with a worker](/recipes/build-an-api/) covers what to put
in the routes.
Worker names are global, like subdomains, and are stored in lowercase. A name
can use letters, numbers, hyphens and underscores. When another account already
has the name, `create()` rejects, so pick another one or use
[`puter.randName()`](/Utils/randName/). The account deploying the worker needs a
verified email address.
Worker names are global, like subdomains. When another account already has the
name, [`create()`](/Workers/create/) rejects, so pick another one or use
[`puter.randName()`](/Utils/randName/). All the naming rules and requirements
are on the [`puter.workers.create()`](/Workers/create/) page.
## Update Its Code
@@ -32,13 +32,9 @@ const granted = await puter.perms.request('appData', {
This returns `true` if the user allows it and `false` if they don't. Once
allowed, later calls return `true` without asking again.
There are three levels of access. Only ask for what you need:
| Scope | Lets your app call |
| --- | --- |
| `read` | `get`, `list` |
| `write` | `set`, `add`, `incr`, `decr`, `update` |
| `delete` | `del`, `remove`, `expire`, `expireAt` |
There are three levels of access, `read`, `write` and `delete`. Only ask for what
you need. The methods each one allows are listed in
[Using another app's data](/Perms/appData/).
`write` doesn't include `delete`, so an app with `write` access can add and
change entries but can't remove them. To ask for more than one, pass an array,
@@ -144,4 +140,4 @@ const setPrivate = (key, value) => puter.kv.set(key, value, { disableSharing: tr
through [Events](/Events/).
- This is for sharing between apps for the same user. To share data between
different users, use a [worker](/recipes/store-server-side-data/) or an
[Events share handle](/Events/onLocal/#sharing-key-value-events-with-another-user).
[Events share handle](/Events/subjects/#sharing-key-value-events-with-another-user).
@@ -66,9 +66,9 @@ const items = await puter.fs.readdir({
});
```
The `sortBy` option takes `name` (the default), `modified`, `type` or `size`,
and `sortOrder` takes `asc` (the default) or `desc`. When you read pages with a
cursor, every page must use the same sort as the first one.
All the sort options are listed on [`puter.fs.readdir()`](/FS/readdir/). When
you read pages with a cursor, every page must use the same sort as the first
one.
## Include Subdirectories
+39 -8
View File
@@ -358,6 +358,14 @@ let sidebar = [
source: '/FS/stat.md',
path: '/FS/stat',
},
{
title: '<code>space()</code>',
page_title: '<code>puter.fs.space()</code>',
title_tag: 'puter.fs.space()',
icon: '/assets/img/function.svg',
source: '/FS/space.md',
path: '/FS/space',
},
{
title: '<code>delete()</code>',
page_title: '<code>puter.fs.delete()</code>',
@@ -414,6 +422,14 @@ let sidebar = [
source: '/FS/listShared.md',
path: '/FS/listShared',
},
{
title: '<code>listSharedByMe()</code>',
page_title: '<code>puter.fs.listSharedByMe()</code>',
title_tag: 'puter.fs.listSharedByMe()',
icon: '/assets/img/function.svg',
source: '/FS/listSharedByMe.md',
path: '/FS/listSharedByMe',
},
{
title: '<code>getShares()</code>',
page_title: '<code>puter.fs.getShares()</code>',
@@ -502,6 +518,14 @@ let sidebar = [
source: '/Events/workers.md',
path: '/Events/workers',
},
{
title: 'Subjects and events',
page_title: 'Subjects and events',
title_tag: 'Events Subjects',
icon: '/assets/img/object.svg',
source: '/Events/subjects.md',
path: '/Events/subjects',
},
],
},
{
@@ -633,14 +657,6 @@ let sidebar = [
source: '/Email.md',
path: '/Email',
children: [
{
title: '<code>sendTransactional()</code>',
page_title: '<code>puter.email.sendTransactional()</code>',
title_tag: 'puter.email.sendTransactional()',
icon: '/assets/img/function.svg',
source: '/Email/sendTransactional.md',
path: '/Email/sendTransactional',
},
{
title: '<code>list()</code>',
page_title: '<code>puter.email.list()</code>',
@@ -657,6 +673,14 @@ let sidebar = [
source: '/Email/get.md',
path: '/Email/get',
},
{
title: '<code>sendTransactional()</code>',
page_title: '<code>puter.email.sendTransactional()</code>',
title_tag: 'puter.email.sendTransactional()',
icon: '/assets/img/function.svg',
source: '/Email/sendTransactional.md',
path: '/Email/sendTransactional',
},
],
},
{
@@ -1374,6 +1398,13 @@ let sidebar = [
source: '/Objects/signinresult.md',
path: '/Objects/signinresult',
},
{
title: '<code>SpaceInfo</code>',
title_tag: 'SpaceInfo',
icon: '/assets/img/object.svg',
source: '/Objects/spaceinfo.md',
path: '/Objects/spaceinfo',
},
{
title: '<code>Speech2TxtResult</code>',
title_tag: 'Speech2TxtResult',