diff --git a/src/backend/controllers/auth/AuthController.test.ts b/src/backend/controllers/auth/AuthController.test.ts index 484cc9cd5..3a543a476 100644 --- a/src/backend/controllers/auth/AuthController.test.ts +++ b/src/backend/controllers/auth/AuthController.test.ts @@ -51,6 +51,11 @@ import { PuterServer } from '../../server.js'; import { createSecret as otpCreateSecret } from '../../services/auth/OTPUtil.js'; import { kvSharePermission } from '../../services/kvShare/kvShares.js'; import { FULL_API_ACCESS } from '../../services/permission/consts.js'; +import { + hasPushConsent, + NOTIFICATIONS_PUSH_PERMISSION, + type PushConsentDeps, +} from '../../services/push/pushConsent.js'; import { setupTestServer } from '../../testUtil.js'; import { resetCardVerificationStatusCache } from '../../util/cardFallback.js'; import { FS_READ_LIMIT } from '../fs/limits.js'; @@ -2179,6 +2184,58 @@ describe('AuthController grant flows', () => { ).toContain(permission); }); + it('grant-user-app: grants an app push consent with notifications:push', async () => { + const app = await server.stores.app.create( + { + name: `tgp-${uuidv4()}`, + title: 'TestGrantPushApp', + index_url: 'https://example.test/index.html', + }, + { ownerUserId: issuer.id }, + ); + const deps = { + stores: server.stores, + services: { permission: server.services.permission }, + } as unknown as PushConsentDeps; + expect(await hasPushConsent(deps, issuer.id, app.uid)).toBe(false); + + // Anything past the bare permission names nothing a prompt describes. + await expect( + inCtx(issuerActor, () => + controller.handleGrantUserApp( + makeReq( + { + app_uid: app.uid, + permission: `${NOTIFICATIONS_PUSH_PERMISSION}:extra`, + }, + { actor: issuerActor }, + ), + makeRes(), + ), + ), + ).rejects.toMatchObject({ + statusCode: 400, + legacyCode: 'permission_not_grantable', + }); + expect(await hasPushConsent(deps, issuer.id, app.uid)).toBe(false); + + const res = makeRes(); + await inCtx(issuerActor, () => + controller.handleGrantUserApp( + makeReq( + { + app_uid: app.uid, + permission: NOTIFICATIONS_PUSH_PERMISSION, + }, + { actor: issuerActor }, + ), + res, + ), + ); + expect(res.body).toEqual({}); + expect(await hasPushConsent(deps, issuer.id, app.uid)).toBe(true); + }); + it('grant-user-app: resolves `origin` to the app when app_uid is omitted', async () => { // The GUI permission dialog and puter.perms.grantOrigin() identify // third-party sites by origin rather than app uid. diff --git a/src/backend/services/permission/appGrantable.test.ts b/src/backend/services/permission/appGrantable.test.ts index 89a1b9e6b..7c5f1e23c 100644 --- a/src/backend/services/permission/appGrantable.test.ts +++ b/src/backend/services/permission/appGrantable.test.ts @@ -95,6 +95,7 @@ describe('parseAppGrantablePermission', () => { }, ], ['events:background', { family: 'backgroundEvents' }], + ['notifications:push', { family: 'pushNotifications' }], ])('accepts %s', (permission, parsed) => { expect(parse(permission)).toEqual(parsed); }); @@ -118,6 +119,9 @@ describe('parseAppGrantablePermission', () => { '*', 'events', 'events:background:extra', + 'notifications', + 'notifications:push:extra', + 'notifications:other', // Modeless, or a mode that does not exist. 'fs', `fs:${FILE}`, diff --git a/src/backend/services/permission/appGrantable.ts b/src/backend/services/permission/appGrantable.ts index c7917323b..b952bcb93 100644 --- a/src/backend/services/permission/appGrantable.ts +++ b/src/backend/services/permission/appGrantable.ts @@ -24,6 +24,7 @@ import { type KvShareAccess, kvShareRoot, } from '../kvShare/kvShares.js'; +import { NOTIFICATIONS_PUSH_PERMISSION } from '../push/pushConsent.js'; import { APP_DATA_CLASSES, APP_DATA_KV_OPS, @@ -62,7 +63,8 @@ export type AppGrantablePermission = appUid: string; segments: string[]; } - | { family: 'backgroundEvents' }; + | { family: 'backgroundEvents' } + | { family: 'pushNotifications' }; /** One component as it is written canonically: `\` as `\\`, `:` as `\C`. */ const escapeComponent = (component: string): string => @@ -117,6 +119,9 @@ export const parseAppGrantablePermission = ( if (permission === 'events:background') { return { family: 'backgroundEvents' }; } + if (permission === NOTIFICATIONS_PUSH_PERMISSION) { + return { family: 'pushNotifications' }; + } const parts = PermissionUtil.split(permission); // Checks that run on the raw string, after this one, match only the