From 2ef5d00d9b69993397e5b4ee6f8fbfd04fccf209 Mon Sep 17 00:00:00 2001 From: Juan Castro Date: Tue, 25 Aug 2026 18:15:15 -0400 Subject: [PATCH] Type is_shared and say who the flag counts stat() and readdir() return FSItemRead, so the is_shared the docs lead with typechecks for TypeScript consumers rather than erroring on FSItem. The docs said "you have shared", but the query has no issuer predicate: a manage delegate's re-share sets the owner's flag too, which is the useful answer and matches getShares(). --- src/docs/src/FS/readdir.md | 2 +- src/docs/src/FS/stat.md | 2 +- src/docs/src/Objects/fsitem.md | 2 +- src/puter-js/index.d.ts | 1 + .../src/modules/FileSystem/operations/readdir.js | 14 +++++++------- .../src/modules/FileSystem/operations/stat.js | 14 +++++++------- src/puter-js/src/modules/FileSystem/types.js | 9 ++++++++- src/puter-js/tests/api/harness/capabilities.ts | 2 +- 8 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/docs/src/FS/readdir.md b/src/docs/src/FS/readdir.md index d26cc3c45..61c731877 100755 --- a/src/docs/src/FS/readdir.md +++ b/src/docs/src/FS/readdir.md @@ -41,7 +41,7 @@ An object with the following properties: A `Promise` that resolves to an array of [`FSItem`](/Objects/fsitem/) objects (files and directories) within the specified directory. -Each item carries `is_shared`: `true` when you have shared it with someone, `false` when you have not, and `null` for items that are not yours. Only shares on the item itself count — the children of a folder you shared report `false`, since the share lives on the folder. Use [`getShares()`](/FS/getShares/) on an item to see who can reach it, including access inherited from a parent. +Each item carries `is_shared`: `true` when it has been shared with someone, `false` when it has not, and `null` for items that are not yours. Only shares on the item itself count — the children of a folder you shared report `false`, since the share lives on the folder. Use [`getShares()`](/FS/getShares/) on an item to see who can reach it, including access inherited from a parent. When the request includes `cursor` (even `null`) or `includeTotal`, the promise instead resolves to a page object: diff --git a/src/docs/src/FS/stat.md b/src/docs/src/FS/stat.md index 86415380d..4ae915e5a 100755 --- a/src/docs/src/FS/stat.md +++ b/src/docs/src/FS/stat.md @@ -37,7 +37,7 @@ An object with the following properties: A `Promise` that resolves to the [`FSItem`](/Objects/fsitem) object of the specified file or directory. -The item carries `is_shared`: `true` when you have shared it with someone, `false` when you have not, and `null` when the item is not yours — whether someone else's file has other recipients is not yours to see. Only shares **on the item itself** count. A file inside a folder you shared is reachable through that folder without being shared itself, so it reports `false`; [`getShares()`](/FS/getShares/) is what reports inherited access. +The item carries `is_shared`: `true` when it has been shared with someone, `false` when it has not, and `null` when the item is not yours — whether someone else's file has other recipients is not yours to see. It covers shares granted by anyone holding `manage` on the item, not only your own, the same way [`getShares()`](/FS/getShares/) does. Only shares **on the item itself** count. A file inside a folder you shared is reachable through that folder without being shared itself, so it reports `false`; `getShares()` is what reports inherited access. With `returnShares: true`, the result also carries `shares` — an array of the same share objects [`getShares()`](/FS/getShares/) returns, including access inherited from a parent folder and unclaimed invitations. It is empty unless you own the item or hold `manage` on it, so asking for it never fails a `stat()` you were otherwise allowed to make. diff --git a/src/docs/src/Objects/fsitem.md b/src/docs/src/Objects/fsitem.md index fa4aab523..58f88f0e2 100755 --- a/src/docs/src/Objects/fsitem.md +++ b/src/docs/src/Objects/fsitem.md @@ -42,7 +42,7 @@ An integer containing the size of the item in bytes. If the item is a directory, #### `is_shared` (Boolean | null) -Whether you have shared this item with anyone: `true` if you have, `false` if you have not, and `null` when the item is not yours. Only shares on the item itself count, not access inherited from a shared parent folder. Set by [`stat()`](/FS/stat/) and [`readdir()`](/FS/readdir/); absent on items obtained any other way. +Whether this item has been shared with anyone: `true` if it has, `false` if it has not, and `null` when the item is not yours. Counts shares granted by anyone holding `manage` on it, and only shares on the item itself — not access inherited from a shared parent folder. Set by [`stat()`](/FS/stat/) and [`readdir()`](/FS/readdir/); absent on items obtained any other way. ## Methods diff --git a/src/puter-js/index.d.ts b/src/puter-js/index.d.ts index 879e39fa8..39be88a7a 100644 --- a/src/puter-js/index.d.ts +++ b/src/puter-js/index.d.ts @@ -96,6 +96,7 @@ export type { export type { CopyOptions, DeleteOptions, + FSItemRead, FSItemWithShares, GetSharesOptions, ListSharedOptions, diff --git a/src/puter-js/src/modules/FileSystem/operations/readdir.js b/src/puter-js/src/modules/FileSystem/operations/readdir.js index 723ea88cd..f7c36d640 100644 --- a/src/puter-js/src/modules/FileSystem/operations/readdir.js +++ b/src/puter-js/src/modules/FileSystem/operations/readdir.js @@ -5,8 +5,8 @@ import mapV2EntryToV1 from '../utils/mapV2EntryToV1.js'; import { fsRequest, parseOperationArgs } from './scaffold.js'; /** @typedef {import('../types.js').ReaddirOptions} ReaddirOptions */ -/** @typedef {import('../../FSItem.js').FSItem} FSItem */ -/** @typedef {import('../../../lib/types.js').ListPage} FSItemPage */ +/** @typedef {import('../types.js').FSItemRead} FSItemRead */ +/** @typedef {import('../../../lib/types.js').ListPage} FSItemPage */ // Listings larger than this are served but never cached. const MAX_CACHE_SIZE = 100 * 1024 * 1024; @@ -166,18 +166,18 @@ const readdirPaged = async function (options) { * @typedef {{ * (options: ReaddirOptions & { stream: true }): AsyncIterableIterator, * (options: ReaddirOptions & ({ cursor: string | null } | { includeTotal: true })): Promise, - * (options: ReaddirOptions): Promise, + * (options: ReaddirOptions): Promise, * ( * path: string, * options?: ReaddirOptions, - * success?: (value: FSItem[]) => void, + * success?: (value: FSItemRead[]) => void, * error?: (reason: unknown) => void, - * ): Promise, + * ): Promise, * ( * path: string, - * success?: (value: FSItem[]) => void, + * success?: (value: FSItemRead[]) => void, * error?: (reason: unknown) => void, - * ): Promise, + * ): Promise, * }} ReaddirOperation */ diff --git a/src/puter-js/src/modules/FileSystem/operations/stat.js b/src/puter-js/src/modules/FileSystem/operations/stat.js index bc275dc94..f0ab675f6 100644 --- a/src/puter-js/src/modules/FileSystem/operations/stat.js +++ b/src/puter-js/src/modules/FileSystem/operations/stat.js @@ -4,13 +4,13 @@ import { fsRequest, parseOperationArgs } from './scaffold.js'; import { toShare } from './shareUtil.js'; /** @typedef {import('../types.js').StatOptions} StatOptions */ +/** @typedef {import('../types.js').FSItemRead} FSItemRead */ /** @typedef {import('../types.js').FSItemWithShares} FSItemWithShares */ -/** @typedef {import('../../FSItem.js').FSItem} FSItem */ /** * @typedef {{ * (options: StatOptions & { returnShares: true }): Promise, - * (options: StatOptions): Promise, + * (options: StatOptions): Promise, * ( * path: string, * options: StatOptions & { returnShares: true }, @@ -20,14 +20,14 @@ import { toShare } from './shareUtil.js'; * ( * path: string, * options?: StatOptions, - * success?: (value: FSItem) => void, + * success?: (value: FSItemRead) => void, * error?: (reason: unknown) => void, - * ): Promise, + * ): Promise, * ( * path: string, - * success: (value: FSItem) => void, + * success: (value: FSItemRead) => void, * error?: (reason: unknown) => void, - * ): Promise, + * ): Promise, * }} StatOperation */ @@ -43,7 +43,7 @@ const MAX_CACHE_SIZE = 20 * 1024 * 1024; * * @this {import('../index.js').PuterJSFileSystemModule} * @param {...unknown} args - * @returns {Promise} + * @returns {Promise} */ const statImpl = async function (...args) { const options = parseOperationArgs(args, ['path']); diff --git a/src/puter-js/src/modules/FileSystem/types.js b/src/puter-js/src/modules/FileSystem/types.js index 75363bf1f..215f2eb08 100644 --- a/src/puter-js/src/modules/FileSystem/types.js +++ b/src/puter-js/src/modules/FileSystem/types.js @@ -174,10 +174,17 @@ * @typedef {StatOptionsOwn & RequestCallbacks} StatOptions */ +/** + * What `stat()` and `readdir()` return: an item plus the sharing state only + * those two report. `is_shared` is null for items the caller does not own. + * + * @typedef {FSItem & { is_shared?: boolean | null }} FSItemRead + */ + /** * A `stat()` result with `returnShares` set. * - * @typedef {FSItem & { shares: Share[] }} FSItemWithShares + * @typedef {FSItemRead & { shares: Share[] }} FSItemWithShares */ /** diff --git a/src/puter-js/tests/api/harness/capabilities.ts b/src/puter-js/tests/api/harness/capabilities.ts index b6eea46b9..cdf1463d4 100644 --- a/src/puter-js/tests/api/harness/capabilities.ts +++ b/src/puter-js/tests/api/harness/capabilities.ts @@ -96,7 +96,7 @@ export const loadPuterJsTestOptions = ( // 402 here. The gate itself is covered against those exact routes in // `controllers/puterai/PuterAIController.subscription.http.test.ts`. meteringEnforcement: { subscriptions: false }, - // Same for rate limits: ~500 tests share one free-tier account. + // ~500 tests share one account; 'unlimited' means paid-base limits. unlimitedMetering: true, };