Puter.js types and docs drift audit - 2026-06-08
Deep audit of the **puter.js SDK** for **TypeScript-types drift** and **documentation drift** against the implementation. Every API family was re-derived from scratch (impl + types + docs read in full).
- **Commit pinned:** `15134c99bf106e3e9a2ff6a69c80e3ab047c48bc` (branch `claude/exciting-lamport-gNnxk`)
- **Sources of truth:** impl `src/puter-js/src/modules/` (+ `src/puter-js/src/index.js`, and `src/gui/src/IPC.js` for the UI family) · types `src/puter-js/types/**` · docs `src/docs/src/**`
**Tag legend:** 🟡 types (impl/docs have it, types don't/wrong) · 🔵 docs (impl/types have it, docs don't/wrong) · 🔴 both · 🐛 impl-bug (advertised-but-ignored param, or phantom method in types) · 🆕/🗑 manifest-drift.
### Step 0 — manifest tripwire
Enumerated disk against the Expected manifest. **No file-level manifest drift** — every impl module, FS op, networking file, UI component, type file, and docs file present on disk is accounted for in the manifest, and every manifest entry exists on disk. (Disk is authority; it agreed with the manifest this run.)
---
## AI
`puter.ai` wired via `registerModule('ai', AI)` (index.js:198).
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| chat | option `provider` | impl forwards `userParams.provider`, but `ChatOptions` omits it and docs don't list it | 🔴 | AI.js:800-802,817 |
| chat | option `image_config` | impl forwards it (PARAMS_TO_PASS); type omits; docs cover it | 🟡 | AI.js:817 |
| chat | option `driver` | impl reads `provider ||= driver`; typed; docs never document `driver` | 🔵 | AI.js:812-814 |
| chat / ChatMessage | `tool_call_id` | used by impl+docs on tool-result messages; `ChatMessage` omits it | 🟡 | chat.md:142 |
| chat / ChatMessage | `cache_control` | docs show it as message passthrough; `ChatMessage` omits it | 🟡 | chat.md:168 |
| chat / ChatResponseChunk | `image` / `type` | streaming docs describe `part.image`/`part.type`; chunk type only has `text?`,`reasoning?` | 🟡 | chat.md:255-271 |
| chat / ChatResponse | `message.images` | Gemini image-gen docs show `message.images[]`; not in types | 🟡 | chat.md:211-226 |
| txt2speech | `engine`→`model` alias | for openai/elevenlabs/gemini, `engine` coerced into `model`; not in docs/types | 🔴 | AI.js:285-317 |
| txt2speech.listEngines / listVoices | types | **neither method declared in `ai.d.ts`** | 🟡 | AI.js:596-690 |
| txt2speech | MAX_INPUT_SIZE | impl hard-caps 3000 chars for all providers; docs claim xAI "15,000 chars" (unreachable) | 🔵/🐛 | AI.js:206,353-355 |
| img2txt | MAX_INPUT_SIZE (10 MB) | undocumented | 🔵 | AI.js:109 |
| speech2speech | camelCase aliases | `voiceId`,`modelId`,`outputFormat`,`voiceSettings`,`fileFormat`,`removeBackgroundNoise`,`optimizeStreamingLatency`,`enableLogging` normalized by impl; types/docs only snake_case (docs even use `removeBackgroundNoise` in an example) | 🔴 | AI.js:414-438 |
| speech2speech | MAX_INPUT_SIZE (25 MB) | undocumented | 🔵 | AI.js:402 |
| speech2txt | `duration`/`words` | xAI docs read these; `Speech2TxtResult` only has `text`,`language`,`segments` | 🟡 | speech2txt.md:128-134 |
| txt2img | `nano-banana`/`nano-banana-pro` aliases | impl rewrites to Gemini model ids; not in types/docs | 🔴 | AI.js:898-905 |
| txt2img | default service | impl defaults to `ai-image`; JSDoc/`@example` claim a Gemini default model not applied | 🔵 | AI.js:897,911-913 |
| txt2img | `input_image_mime_type`/`input_images` | docs reference them; `input_image_mime_type` missing from type | 🟡 | txt2img.md:60,106,196 |
| txt2vid | `duration`→`seconds` alias | impl maps it; docs only document `seconds` | 🔵 | AI.js:977-979 |
| txt2vid | `last_frame` | Veo docs document it; not in type (`input_reference`/`reference_images`/`frame_images` are) | 🟡 | txt2vid.md:64 |
| ToolCall | `type` field | docs/OpenAI convention include `type:"function"`; `ToolCall` + Objects/toolcall.md omit it | 🔴 | ai.d.ts:9-12 |
| ChatResponse / chunk obj-docs | `choices`/`reasoning` | type has them; Objects/*.md omit | 🔵 | ai.d.ts:34,39 |
---
## Apps
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| list/get/create/update | return decoration | returned `App` is decorated with `getUsers()`/`users()` async-iterator (documented in app.md) but missing from the `App` type | 🟡 | Apps.js:43-48 |
| get | object-only overload `get({...})` | impl supports it; types only declare `get(name, options?)` | 🟡 | Apps.js:190-192 |
| create | `title` "(required)" | doc marks required; impl defaults `title = args[2] ?? args[0]`, type `title?` | 🔵 | Apps.js:90 |
| delete | return `{success:true}` | doc overstates; type `{success?:boolean}`, impl returns raw driver result | 🔵 | Apps.js:203 |
| checkName | docs | impl+types have it; no doc page / not in Apps.md | 🔵 | Apps.js:206 |
| getDeveloperProfile | docs | impl+types have it; undocumented | 🔵 | Apps.js:232 |
---
## Auth
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| getGlobalUsage | existence | impl has it (`/metering/globalUsage`); **missing from type AND docs** | 🔴 | Auth.js:338 |
| User | `email` field | `requestEmail` reads `whoami.email`, but `User` type AND user.md both omit `email` | 🔴 | Perms.js:154 |
| getUser | positional callbacks | impl supports `getUser(success, error)`; type only declares options form | 🟡 | Auth.js:187-195 |
| whoami | docs | impl+type have it; no doc (internal-ish) | 🔵 | Auth.js:211 |
| DetailedAppUsage | index signature | `total:number` + `[key:string]:APIUsage` is unsound TS | 🟡 | auth.d.ts:42 |
---
## Perms
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| ALL `request*` methods | types | `perms.d.ts` declares **none** of the 18+ documented `request*` methods (only grant/revoke/group plumbing) | 🟡 | Perms.js:123-348 |
| requestReadAppRootDir / requestWriteAppRootDir | existence | impl has both (public); not in docs, not in Perms.md, not in types | 🔴 | Perms.js:333,346 |
| grant*/revoke*/group methods | docs | typed+implemented; no doc pages | 🔵 | Perms.js:37-118 |
---
## FileSystem (FS)
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| read | `cache` option | impl reads `options.cache`; missing from `ReadOptions` type AND read doc | 🔴 | operations/read.js:46 |
| readdir | `no_subdomains` | impl sends it; `ReaddirOptions` missing it (has `no_thumbs`,`no_assocs`); docs omit all three | 🔴 | operations/readdir.js:57-59 |
| stat | `returnWorkers` alias | impl: `return_subdomains = returnSubdomains || returnWorkers`; type+docs only have `returnSubdomains` | 🔴 | operations/stat.js:54 |
| upload | `generateThumbnails`/`thumbnailGenerator`/`thumbnail` | impl reads all; missing from `UploadOptions` type AND docs | 🔴 | operations/upload.js:571,690,1313 |
| upload | progress/callback surface | `init/start/progress/abort/success/error` typed but entirely undocumented | 🔵 | operations/upload.js:405 |
| revokeReadURL | existence | public (index.js:44); not in `FS` type, no doc | 🔴 | operations/revokeReadUrl.js:10 |
| readdirSubdomains | existence | public (index.js:46); not in `FS` type, no doc | 🔴 | operations/readdirSubdomains.js:11 |
| sign | docs | public (index.js:42), typed; no doc page | 🔵 | operations/sign.js:87 |
| move | `dedupeName` | doc lists it but impl never reads/sends dedupe for move (phantom doc option) | 🔵 | operations/move.js |
| copy | `createMissingParents`/`newMetadata` | impl reads+sends; docs omit (types ok) | 🔵 | operations/copy.js:17-18 |
| mkdir | `rename`/`recursive` aliases | impl reads them; docs list only `dedupeName`/`createMissingParents` | 🔵 | operations/mkdir.js:51-54 |
| readdir/stat | `consistency` | impl supports it; docs omit | 🔵 | operations/readdir.js:29 |
| write | ArrayBuffer/TypedArray data | impl+type accept them; doc says `String\|File\|Blob` only | 🔵 | operations/write.js:48 |
| getReadURL | `expiresIn` | impl default is string `'24h'`, type `string`; doc wrongly says "milliseconds (Number)" | 🔵 | operations/getReadUrl.js:4 |
| space | doc example | space.md example calls `puter.space()` instead of `puter.fs.space()` | 🔵 | operations/space.js |
| FSItem (obj-doc) | `parent_id`/`parent_uid`/`writable`/`is_dir` | fsitem.md documents fields the FSItem class never sets; real props are `isDirectory`,`readURL`,`writeURL`,`metadataURL`,`uuid` | 🔴 | FSItem.js:3-58 |
---
## KV
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| MAX_VALUE_SIZE | constant | impl `399*1024` (399 KB); set.md says **400 KB** | 🔵 | KV.js:36 |
| get | null vs undefined | get.md says missing key → `null`; impl+type resolve to **`undefined`** | 🔵 | KV.js:253 |
| set | batch overload | `set([{key,value}], …)` / `set({items})` real but absent from types AND set.md | 🔴 | KV.js:124 |
| add | default value=1 | `add(key)` silently adds 1; undocumented in add.md | 🔵 | KV.js:386 |
| clear | alias | `clear = flush` in types; no doc page / not in KV.md | 🔵 | KV.js:676 |
---
## Workers
`router.md`/`types.md` describe the deployed-worker runtime (verified sane, not mapped to Workers.js).
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| create | 3rd param `appName` + `sandbox` | impl accepts string (app-name lookup) OR `{sandbox:false}` object; type declares only `appName?:string`, docs omit the param entirely | 🔴 | Workers.js:10,21,24,37 |
| getLoggingHandle | docs | public, in `.d.ts`; no doc page / not in Workers.md | 🔵 | Workers.js:137 |
| exec | `x-puter-no-auth` escape hatch | suppresses auto `puter-auth` injection; undocumented | 🔵 | Workers.js:70,73 |
| exec | input type | docs say `workerURL` (String) only; impl/type accept any `RequestInfo`/`Request`/`URL` | 🔵 | Workers.js:69 |
---
## Hosting
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| delete | return | type `Promise<boolean>` + docs say resolves `true`, but impl returns raw driver result (no coercion) | 🔴 | Hosting.js:135 |
---
## Networking
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| Socket / TLSSocket `error` | payload type | impl emits an **Error object**; type+docs say `reason: (string)` — every error handler example prints the wrong thing | 🔴 | PSocket.js:52; PTLS.js:67,86 |
| Socket `drain` event | never emitted | registered + in `SocketEvent` union but never fired (dead event in public type) | 🟡 | PSocket.js:16 |
| TLSSocket `on()` aliasing | `open/data/close` → `tls*` | impl silently remaps; inherited `open/data/close` overloads misleading on TLS; undocumented | 🔵 | PTLS.js:92 |
| generateWispV1URL | docs | typed + wired (index.js:627); undocumented (likely internal) | 🔵 | index.js:627 |
---
## Peer
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| PuterPeerServer.message | phantom method | type declares public `message()`; impl method is private `#message` (not callable) | 🟡 | Peer.js:104 |
| PuterPeerConnectionErrorEvent.error | type | typed `string`, but impl stores an **Error**; docs read `event.error?.message` | 🟡 | Peer.js:35 |
| setRemoteDescription / addIceCandidate | return type | impl `async` → `Promise<void>`; type declares `void` (can't `await` per types) | 🟡 | Peer.js:326,330 |
| PuterPeerConnectionMessageEvent.data | type | impl forwards `string\|ArrayBuffer\|Blob`; type omits `Blob` | 🟡 | Peer.js:13 |
---
## UI (impl: UI.js + IPC.js)
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| exit | placement | typed on `UI` class, but implemented on root `puter` (`puter.exit`); `puter.ui.exit` does not exist. UI.md also lists it as `puter.ui.exit()` | 🐛/🔵 | index.js:1031 |
| broadcast / listenForBroadcast | phantom methods | declared on `UI` type; no implementation (use `on(name, handler)`) | 🐛 | ui.d.ts:195-196 |
| FILE_SAVE_CANCELLED / FILE_OPEN_CANCELLED | phantom getters | typed instance getters; are module-private constants, no public getters | 🐛 | ui.d.ts:198-199 |
| on | `connection` event | in `#eventNames` and emitted (`{conn, accept, reject}`); not in types or docs | 🔴 | UI.js:250,568 |
| showSaveFilePicker | 3rd `type` param | `'url'`/`'move'`/`'copy'` modes honored by impl/IPC; missing from types AND docs | 🔴 | UI.js:898; IPC.js:1697 |
| disableMenuItem / enableMenuItem | existence | implemented + IPC-handled; absent from types and docs | 🔴 | UI.js:1101; IPC.js:734 |
| toggleWindow | existence | implemented; absent from types and docs | 🔴 | UI.js:1074 |
| instancesOpen | existence | implemented (IPC `getInstancesOpen`); absent from types and docs | 🔴 | UI.js:722; IPC.js:302 |
| requestPermission | existence | implemented (returns boolean); absent from types and docs | 🔴 | UI.js:1091; IPC.js:1292 |
| connectToInstance | existence | implemented (returns AppConnection); absent from types and docs | 🔴 | UI.js:1330 |
| showColorPicker | `defaultValue`/`default` aliases | GUI honors `defaultValue ?? defaultColor ?? default`; type only declares `defaultColor` | 🟡 | UIWindowColorPicker.js:77 |
| showFontPicker | `defaultFont` | typed, but GUI reads only `options.default` → `defaultFont` is a no-op in-app | 🐛 | UIWindowFontPicker.js:47 |
| alert | `type` enum | types/docs say `{primary,success,info,warning,danger}`; GUI keys on `{error,warning,info,success}` (`danger`/`primary` fall through; working error key is `error`) | 🟡 | UIAlert.js:45-53 |
| prompt | `options` param | 3rd `options` ({defaultValue}) honored+typed; prompt.md omits it | 🔵 | UI.js:734; IPC.js:244 |
| createWindow | return shape | resolves `{id}` handle (typed); createWindow.md documents no return | 🔵 | UI.js:1351 |
| setMenuItemIcon/IconActive/Checked | docs | impl+types; no doc pages / not in UI.md | 🔵 | UI.js:1109 |
| requestUpgrade / launchApp options | docs | typed; under-documented | 🔵 | UI.js:892,1287 |
| socialShare | return type | typed `void`; impl returns a `Promise` | 🟡 | UI.js:728 |
---
## Utils
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| print | `escapeHTML` option | impl+type have it; print.md documents only `code` | 🔵 | index.js:1249 |
| env | enum values | env.md lists `app`/`web`/`gui`; impl/type also support `web-worker`,`service-worker`,`nodejs` | 🔵 | index.js:354-389 |
---
## Drivers (types-only verify; no docs expected)
| Function | Item | Issue | Tag | impl ref |
|---|---|---|---|---|
| call | 2-arg overload | impl supports `call(iface_name, parameters)` (e.g. `puter.drivers.call('ipgeo', {ip})`); type declares only 3-arg/4-arg overloads | 🟡 | Drivers.js:262-265 |
---
## OS
No drift found; no docs family expected and none appeared. `user({query})` honored and typed.
---
## (a) Highest-impact shortlist
1. **🐛 `puter.ui.exit` / `broadcast` / `listenForBroadcast` / `FILE_*_CANCELLED` are phantom typed members** — they don't exist at runtime. `exit` actually lives on root `puter`. TS users hit `TypeError`. (ui.d.ts:154,195-199)
2. **🐛 `FSItem.copy()` / `FSItem.move()` silently drop their `overwrite`/`auto_rename`/`new_name` arguments** — they pass booleans into an options-object slot, so the args have no effect and a boolean can land in the success-callback slot. (FSItem.js:89-95)
3. **🐛 `requestWriteAppRootDir` is broken** — `#requestAppRootDir` ignores its `access` arg and always requests **read** (`access:'read'` hardcoded, perm string `…:read`). Write access is unobtainable. (Perms.js:350-369)
4. **🔴 Multiple public methods exist with zero type+doc coverage:** FS `revokeReadURL`/`readdirSubdomains`; UI `disableMenuItem`/`enableMenuItem`/`toggleWindow`/`instancesOpen`/`requestPermission`/`connectToInstance`; Auth `getGlobalUsage`.
5. **🟡 `perms.d.ts` is missing the entire `request*` API surface** (18+ documented methods) — only grant/revoke/group plumbing is typed.
6. **🔴 Socket/TLSSocket `error` payload is an `Error`, not the documented/typed `string`** — affects the most-used socket event and every example. (PSocket.js:52)
7. **🔴 `workers.create` 3rd param (`appName` + `sandbox`) is behavior-changing yet undocumented and mistyped.** (Workers.js:24)
8. **🔵/🐛 KV: `get` returns `undefined` not `null` (docs wrong); `MAX_VALUE_SIZE` is 399 KB not 400 KB; `set` batch form is untyped+undocumented; the size limit is only enforced for strings/arrays.**
9. **🔴 fsitem.md documents fields the FSItem class never produces** (`parent_id`/`parent_uid`/`writable`/`is_dir`) while omitting the real ones.
10. **🔴 AI: speech2speech camelCase aliases & txt2img model aliases are undocumented/untyped; `txt2speech.listEngines`/`listVoices` are entirely missing from `ai.d.ts`.**
## (b) 🐛 impl-bug bucket (code fixes, not doc/type edits)
- **UI:** `exit` (phantom on `UI` type — lives on root `puter`), `broadcast`, `listenForBroadcast`, `FILE_SAVE_CANCELLED`/`FILE_OPEN_CANCELLED` getters — all phantom in `ui.d.ts`. `FontPickerOptions.defaultFont` is advertised but ignored by the GUI (reads `options.default`). (ui.d.ts:154,195-199; UIWindowFontPicker.js:47)
- **FSItem.copy() / FSItem.move()** pass booleans into the options-object slot → `overwrite`/`auto_rename`/`new_name` silently dropped, boolean lands in success-callback slot. (FSItem.js:89-95; copy.js:15; move.js:17)
- **Perms `#requestAppRootDir`:** ignores `access` arg (hardcodes `read`) so `requestWriteAppRootDir` requests read; references undefined var `app_or_uuid` (should be `app_uid`); error typo "strinkg". (Perms.js:350-369)
- **AI:** `ChatOptions.vision` is a phantom option (`chat()` never reads `userParams.vision`); docs' `text_verbosity` alias is never read; `speech2speech` `provider` option is stripped and ignored (hardcoded `elevenlabs-voice-changer`); txt2speech 3000-char cap makes the documented xAI "15,000 chars" unreachable. (AI.js:733,817,484)
- **KV:** value-size check uses `args.value.length` → effectively only enforces `MAX_VALUE_SIZE` for strings/arrays, not the serialized JSON payload actually sent; `incr`/`decr`/`add`/`expire`/`expireAt` deref `options.key.length` before validating the key → raw `TypeError` instead of `key_undefined`. (KV.js:208,328)
- **Workers:** `delete` builds `new Error("Worker doesn't exist")` but never `throw`s it (dead error); `getLoggingHandle` calls `make_driver_method([], …)(authToken, workerName)` with empty arg_defs → both positional args dropped. (Workers.js:119-123,138)
- **Hosting:** `delete` returns raw driver result though type/docs promise `true`; `update` with a non-string first arg silently sends `{object:undefined}` (asymmetric with `create`). (Hosting.js:135,83-104)
- **Networking:** `PSocket.write` ArrayBuffer branch calls `data.write(...)` on the ArrayBuffer (throws); `PWispHandler._continue()` is called with no arg → `streamMap.get(undefined)` throws (buffered-write flush dead); `setup()` reconnect uses `setTimeout(setup(), 1000)` (calls immediately instead of scheduling). (PSocket.js:74-76; PWispHandler.js:51,23)
- **Peer:** `PuterPeerServer.message` is a phantom public method in the type (real method is private `#message`). (Peer.js:104)
- **Apps:** `getDeveloperProfile` has duplicated `options` decls + a nested unreturned Promise (works by accident); `update('name')` with no attributes dereferences `object_raw.name` on `undefined` → TypeError. (Apps.js:246-268,155-156)
## (c) Manifest drift
**None.** Disk file set exactly matches the Expected manifest this run (no 🆕 new files, no 🗑 removed files). The drift above is all within-file API drift.
---
<sub>Pinned to `15134c99bf106e3e9a2ff6a69c80e3ab047c48bc`. Each finding is reproducible against that tree. Excluded as not-drift: `_`-suffixed internal methods, cosmetic param-name differences, intentionally-`unknown` provider/driver/worker stream payloads, and internal plumbing (`Util.rpc`, `setAuthToken`/`setAPIOrigin`).</sub>
https://claude.ai/code/session_019B8Wy8kJfbwLNwtLtcBq3v
0 条评论