Files
puter/src/backend/core/actor.test.ts
T
Juan Fernando CastroandDaniel Salazar d202be10a9 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>
2026-08-08 04:04:07 -07:00

105 lines
3.8 KiB
TypeScript

/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { describe, expect, it } from 'vitest';
import {
assertResolvedActor,
makeActor,
SYSTEM_ACTOR,
userRelatedActor,
type Actor,
} from './actor';
describe('makeActor / effectiveApp', () => {
const user = { uuid: 'u-1', id: 1, username: 'u' };
it('resolves a plain user actor to no app', () => {
expect(makeActor({ user }).effectiveApp).toBeNull();
});
it("resolves an app-under-user actor to its own app", () => {
expect(makeActor({ user, app: { uid: 'app-1' } }).effectiveApp).toEqual({
uid: 'app-1',
});
});
it("resolves a token to the app that issued it", () => {
// The whole point: a token actor carries no `app` of its own, so
// anything reading `app` sees a bare user token and skips app gating.
const issuer = makeActor({ user, app: { uid: 'app-1' } });
const token = makeActor({
user,
accessToken: { uid: 'tok-1', issuer },
});
expect(token.app).toBeUndefined();
expect(token.effectiveApp).toEqual({ uid: 'app-1' });
});
it('collapses a chain of tokens in one hop', () => {
const issuer = makeActor({ user, app: { uid: 'app-1' } });
const inner = makeActor({ user, accessToken: { uid: 't1', issuer } });
const outer = makeActor({
user,
accessToken: { uid: 't2', issuer: inner },
});
expect(outer.effectiveApp).toEqual({ uid: 'app-1' });
});
it('resolves a user-issued token to no app', () => {
const issuer = makeActor({ user });
const token = makeActor({
user,
accessToken: { uid: 'tok-1', issuer, fullAccess: true },
});
expect(token.effectiveApp).toBeNull();
});
it('drops the app when narrowing to the underlying user', () => {
const app = makeActor({ user, app: { uid: 'app-1' } });
expect(userRelatedActor(app).effectiveApp).toBeNull();
});
it('resolves the system actor', () => {
expect(SYSTEM_ACTOR.effectiveApp).toBeNull();
});
});
describe('assertResolvedActor', () => {
it('passes a resolved actor through unchanged', () => {
const actor = makeActor({ user: { uuid: 'u-1' } });
expect(assertResolvedActor(actor)).toBe(actor);
// `null` is a resolved answer, not a missing one.
expect(assertResolvedActor({ user: {}, effectiveApp: null })).toEqual({
user: {},
effectiveApp: null,
});
});
it('throws on an actor that skipped makeActor', () => {
// The field is optional so pre-existing literals still compile, which
// means an unresolved one can reach a gate. Fail loudly at the edge
// rather than let a gate read `undefined` as "no app" and wave it
// through — an app-under-user actor is the dangerous case.
const unresolved = { user: { uuid: 'u-1' }, app: { uid: 'app-1' } };
expect(() => assertResolvedActor(unresolved as Actor)).toThrow(
/effectiveApp/,
);
});
});