feat: let apps use another app's data with user consent (#3516)

* feat(perms): add cross-app app-data permission vocabulary

* feat(perms): sweep app grants by permission prefix

* feat(perms): resolve and withdraw cross-app data grants

* feat(kv): support an authorized namespace override and per-key privacy

* feat(kv): gate cross-app KV access behind app-data grants

* feat(fs): allow cross-app AppData access and require a scope to delete

* feat(auth): accept permission lists and gate app-data grants

* feat(perms): add requestAppData to the puter.js SDK

* feat(gui): carry permission lists through the IPC and popup transports

* feat(gui): describe cross-app data requests in the consent dialog

* docs: document requestAppData and per-entry KV privacy

* perf(perms): sweep cross-app grants only for origin-bootstrapped apps

* fix(gui): stop double-encoding cross-app consent text

* fix(perms): close three gaps in cross-app grant enforcement

* fix(kv): meter and batch the per-entry privacy probe

* fix(perms): resolve app identifiers and scopes more strictly in the SDK

* test(perms): cover the cross-app consent flow end to end

* fix: small missing token resolution for app

also adds the same exclusion for the batchPut api, small change

* fix: make resolved actor optional

---------

Co-authored-by: Daniel Salazar <daniel.salazar@puter.com>
This commit is contained in:
Juan Fernando Castro
2026-08-08 04:04:07 -07:00
committed by GitHub
co-authored by Daniel Salazar
parent 2c17476c07
commit d202be10a9
52 changed files with 4757 additions and 299 deletions
+23 -1
View File
@@ -6,7 +6,7 @@ platforms: [websites, apps, nodejs, workers]
When passed a key and a value, will add it to the user's key-value store, or update that key's value if it already exists.
<div class="info">Each app has its own private key-value store within each user's account. Apps cannot access the key-value stores of other apps - only their own.</div>
<div class="info">Each app has its own key-value store within each user's account. Another app can only reach it if the user explicitly grants that with <a href="/Perms/requestAppData/">puter.perms.requestAppData()</a> — and never for entries you write with <code>disableSharing</code>.</div>
## Syntax
@@ -32,6 +32,14 @@ A string containing the value you want to give the key you are creating/updating
A number containing when the key should expire in timestamp seconds.
#### `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.requestAppData()`](/Perms/requestAppData/). 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.
Your own app reads and writes the entry normally. Writing the same key again without the flag makes it shareable once more, since `set` replaces the whole entry.
#### `items` (Array) (batch only)
An array of `{ key, value, expireAt? }` objects, set in a single request. Each `key` is required and follows the same **1 KB** key / **400 KB** value limits. You can pass the array directly (`set([...])`) or wrapped in an object (`set({ items: [...] })`).
@@ -44,6 +52,20 @@ A `Promise` that will resolves to `true` when the key-value pair has been create
## Examples
<strong class="example-title">Store a value no other app can ever read</strong>
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
puter.kv.set('accessToken', 'secret-value', { disableSharing: true })
.then(() => puter.print('Stored privately'));
</script>
</body>
</html>
```
<strong class="example-title">Create a new key-value pair</strong>
```html;kv-set
+33 -1
View File
@@ -4,7 +4,7 @@ description: Request permissions to access user data and resources with Puter.js
platforms: [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, and subdomains.
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.
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.
@@ -15,6 +15,7 @@ When requesting permissions, users will be prompted to grant or deny access. If
<div class="example-group" data-section="request-desktop"><span>Request Desktop Access</span></div>
<div class="example-group" data-section="request-documents"><span>Request Documents Access</span></div>
<div class="example-group" data-section="request-apps"><span>Request Apps Access</span></div>
<div class="example-group" data-section="request-app-data"><span>Use Another App's Data</span></div>
</div>
<div class="example-content" data-section="request-email" style="display:block;">
@@ -123,6 +124,33 @@ When requesting permissions, users will be prompted to grant or deny access. If
</div>
<div class="example-content" data-section="request-app-data">
#### Use another app's saved data
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<button id="request-app-data">Use another app's data</button>
<script>
document.getElementById('request-app-data').addEventListener('click', async () => {
const other = await puter.apps.get('contacts');
const granted = await puter.perms.requestAppData(other.uid, 'read');
if (granted) {
const keys = await puter.kv.list({ appUuid: other.uid });
puter.print(`${other.title} has ${keys.length} key(s)`);
} else {
puter.print('Permission denied');
}
});
</script>
</body>
</html>
```
</div>
## Functions
These permission features are supported out of the box when using Puter.js:
@@ -160,6 +188,10 @@ These permission features are supported out of the box when using Puter.js:
- **[`puter.perms.requestReadApps()`](/Perms/requestReadApps/)** - Request read access to the user's apps
- **[`puter.perms.requestManageApps()`](/Perms/requestManageApps/)** - Request write (manage) access to the user's apps
### Other Apps' Data
- **[`puter.perms.requestAppData()`](/Perms/requestAppData/)** - Request permission to use another app's key-value data and `AppData` files
### Subdomains Management
- **[`puter.perms.requestReadSubdomains()`](/Perms/requestReadSubdomains/)** - Request read access to the user's subdomains
+146
View File
@@ -0,0 +1,146 @@
---
title: puter.perms.requestAppData()
description: Request permission to use another app's data — its key-value store and its AppData files.
platforms: [websites, apps]
---
Request permission for your app to use another app's data belonging to the signed-in user: that app's key-value namespace, its `AppData` directory, or both. A calendar might read a contacts app's entries to show birthdays, and add an invite the user can later cancel from either app.
The user is prompted once and sees exactly which apps and which kinds of access are involved. If the permission has already been granted the user is not prompted and `true` is returned. If the user declines, `false` is returned.
On a website, sign the user in to your site first with [`puter.auth.signIn()`](/Auth/signIn/). This method reads the signed-in user's identity before it can prompt, so for a signed-out visitor it rejects with `Unauthorized` and no prompt is shown. Answering a permission prompt does not by itself sign the user in to your site, so guard the call:
```js
if (!puter.authToken) await puter.auth.signIn();
```
## Syntax
```js
puter.perms.requestAppData(appIdentifier, scopes)
```
## Parameters
#### `appIdentifier` (String | Object) (required)
The app whose data you want to use. Either its uid (`app-…`), its registered name, or an object carrying one: `{ uid: 'app-…' }` or `{ name: 'contacts' }`.
#### `scopes` (String | Array | Object) (required)
What access to ask for. Three equivalent forms:
- **A single word** applied to both stores: `'read'`, `'write'`, or `'delete'`.
- **An array of `store:name` pairs**: `['kv:get', 'fs:read']`.
- **An object per store**: `{ kv: ['get', 'set'], fs: 'read' }`.
`store` is `kv` (the app's key-value data) or `fs` (its files under `AppData`).
`name` is either an access class or a single key-value operation:
| Class | Covers |
| --- | --- |
| `read` | `get`, `list` |
| `write` | `set`, `add`, `incr`, `decr`, `update` |
| `delete` | `del`, `remove`, `expire`, `expireAt` |
**`delete` is separate from `write`.** An app granted `write` can add and change entries but cannot remove any — ask for `delete` explicitly when it needs to. Emptying another app's whole key-value store is never available at any scope.
## Return value
A `Promise` that resolves to:
- `true` - If your app may now use that data
- `false` - If the user declined
The promise rejects if the named app does not exist, or if a scope is misspelled.
## Examples
<strong class="example-title">Read another app's data</strong>
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<button id="request">Use Contacts' data</button>
<script>
document.getElementById('request').addEventListener('click', async () => {
const contacts = await puter.apps.get('contacts');
const granted = await puter.perms.requestAppData(contacts.uid, 'read');
if (!granted) {
puter.print('Permission denied');
return;
}
// Read from the other app's key-value namespace.
const birthdays = await puter.kv.get('birthdays', { appUuid: contacts.uid });
puter.print(`Birthdays: ${JSON.stringify(birthdays)}`);
});
</script>
</body>
</html>
```
<strong class="example-title">Add an entry, and be able to remove it later</strong>
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<button id="book">Book, then cancel</button>
<script>
document.getElementById('book').addEventListener('click', async () => {
const contacts = await puter.apps.get('contacts');
// Writing and deleting are separate scopes, so ask for both.
const granted = await puter.perms.requestAppData(contacts.uid, {
kv: ['set', 'del'],
});
if (!granted) return;
await puter.kv.set('invite:42', { when: 'friday' }, { appUuid: contacts.uid });
puter.print('Invite added');
await puter.kv.del('invite:42', { appUuid: contacts.uid });
puter.print('Invite cancelled');
});
</script>
</body>
</html>
```
<strong class="example-title">Read another app's files</strong>
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<button id="files">List Contacts' files</button>
<script>
document.getElementById('files').addEventListener('click', async () => {
const contacts = await puter.apps.get('contacts');
const granted = await puter.perms.requestAppData(contacts.uid, ['fs:read']);
if (!granted) return;
const user = await puter.auth.getUser();
const items = await puter.fs.readdir(`/${user.username}/AppData/${contacts.uid}`);
puter.print(`${items.length} file(s)`);
});
</script>
</body>
</html>
```
## Keeping your own data private
Another app can only reach your data if the user grants it, but the user cannot see what a key-value namespace holds before answering. If your app stores something no other app should ever read — a cached OAuth token, a licence key — mark it private when you write it:
```js
await puter.kv.set('googleRefreshToken', token, { disableSharing: true });
```
A private entry is invisible to every other app: reads return nothing, listings omit it, and writes and deletes are refused — regardless of what the user has granted. Your own app reads and writes it normally, and writing the key again without the flag makes it shareable once more.
To keep *all* of your app's data out of this feature, set `share_app_data` to `false` in your app's metadata. Requests naming your app are then refused and the user is never prompted.
## Notes
Granted access is scoped to the user who granted it, and only to the two stores above — it does not extend to that app's source, settings, or anything outside their per-user data.
Access ends automatically when the target app is deleted. Grants are also withdrawn if the app is later re-created under the same identifier, so a new owner of that identifier does not inherit consent the user gave its predecessor.
@@ -0,0 +1,22 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<button id="request-app-data">Use another app's data</button>
<script>
document.getElementById('request-app-data').addEventListener('click', async () => {
// Any app the signed-in user has; swap 'contacts' for your own.
const other = await puter.apps.get('contacts');
const granted = await puter.perms.requestAppData(other.uid, 'read');
if (!granted) {
puter.print("Permission denied");
return;
}
// Read from that app's key-value namespace.
const keys = await puter.kv.list({ appUuid: other.uid });
puter.print(`${other.title} has ${keys.length} key(s): ${keys.join(', ')}`);
});
</script>
</body>
</html>