Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"httpbis-digest-headers": "^1.0.0",
"iso8601-duration": "^2.1.2",
"loglevel": "^1.9.2",
"permissions-policy-allows-feature": "^0.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"safe-buffer": "5.2.1",
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/background/services/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,14 @@ export class Background {
);
return;

case 'IS_MONETIZATION_ALLOWED':
return success(
this.monetizationService.isMonetizationAllowed(
message.payload,
sender,
),
);

// endregion

// region App
Expand Down
28 changes: 27 additions & 1 deletion src/background/services/monetization.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { Runtime, Tabs } from 'webextension-polyfill';
import type {
IsMonetizationAllowedPayload,
PayWebsitePayload,
PayWebsiteResponse,
ResumeMonetizationPayload,
StartMonetizationPayload,
StopMonetizationPayload,
} from '@/shared/messages';
import { PaymentSession } from './paymentSession';
import { computeRate, getSender, getTabId } from '@/background/utils';
import { computeRate, getSender, getTab, getTabId } from '@/background/utils';
import { isOutOfBalanceError } from './openPayments';
import {
OUTGOING_PAYMENT_POLLING_MAX_ATTEMPTS,
Expand All @@ -21,6 +22,7 @@ import {
removeQueryParams,
transformBalance,
} from '@/shared/helpers';
import { PermissionsPolicy } from 'permissions-policy-allows-feature';
import type { AmountValue, PopupStore, Storage } from '@/shared/types';
import type { OutgoingPayment } from '@interledger/open-payments';
import type { Cradle } from '@/background/container';
Expand Down Expand Up @@ -550,6 +552,30 @@ export class MonetizationService {
};
}

isMonetizationAllowed(
payload: IsMonetizationAllowedPayload,
sender: Runtime.MessageSender,
): boolean {
const tab = getTab(sender);

// This will be stored in tabState likely
// https://github.com/interledger/web-monetization-extension/issues/959
// We also want to allow/disallow monetization on host-document in future with this.
const permissionsPolicy = new PermissionsPolicy({
headerValue: '', // we'll get this via webRequest in future
origin: tab.url,
defaultAllowlist: { monetization: "'self'" },
});

// get the permissions policy for given iframe
const { allowAttribute: allow, origin } = payload;
const iframePermissionsPolicy = permissionsPolicy.inherit({
allow,
origin,
});
return iframePermissionsPolicy.allowsFeature('monetization', origin);
}

private async adjustSessionsAmount(
sessions: PaymentSession[],
rate: AmountValue,
Expand Down
1 change: 1 addition & 0 deletions src/content/__tests__/monetizationLinkManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const msg: MessageMocks = {
START_MONETIZATION: jest.fn(),
STOP_MONETIZATION: jest.fn(),
TAB_FOCUSED: jest.fn(),
IS_MONETIZATION_ALLOWED: jest.fn(),
};
const messageMock = jest.spyOn(messageManager, 'send');
// @ts-expect-error let it go
Expand Down
55 changes: 48 additions & 7 deletions src/content/services/frameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ export class FrameManager {
private frameAllowAttrObserver: MutationObserver;
private frames = new Map<
HTMLIFrameElement,
{ frameId: string | null; requestIds: string[] }
{
frameId: string | null;
requestIds: string[];
allowAttribute: string;
origin: string;
}
>();

constructor({ window, document, logger, message }: Cradle) {
Expand Down Expand Up @@ -78,9 +83,21 @@ export class FrameManager {
}
const hasTarget = this.frames.has(target);
const typeSpecified =
target instanceof HTMLIFrameElement && target.allow === 'monetization';
target instanceof HTMLIFrameElement &&
target.allow.includes('monetization');

let allowedByPermissionsPolicy = false;
if (!hasTarget && typeSpecified) {
const res = await this.message.send('IS_MONETIZATION_ALLOWED', {
allowAttribute: target.allow,
origin: new URL(target.src, location.origin).origin,
});
if (res.success && res.payload) {
allowedByPermissionsPolicy = true;
}
}

if (!hasTarget && typeSpecified && allowedByPermissionsPolicy) {
await this.onAddedFrame(target);
handledTags.add(target);
} else if (hasTarget && !typeSpecified) {
Expand All @@ -97,6 +114,8 @@ export class FrameManager {
this.frames.set(frame, {
frameId: null,
requestIds: [],
allowAttribute: frame.allow,
origin: new URL(frame.src, location.origin).origin,
});
}

Expand Down Expand Up @@ -181,7 +200,7 @@ export class FrameManager {
private bindMessageHandler() {
this.window.addEventListener(
'message',
(event: MessageEvent<ContentToContentMessage>) => {
async (event: MessageEvent<ContentToContentMessage>) => {
const { message, payload, id } = event.data;
if (!HANDLED_MESSAGES.includes(message)) {
return;
Expand All @@ -201,15 +220,26 @@ export class FrameManager {
this.frames.set(frame, {
frameId: id,
requestIds: [],
allowAttribute: frame.allow,
origin: new URL(frame.src, location.origin).origin,
});
return;

case 'IS_MONETIZATION_ALLOWED_ON_START':
case 'IS_MONETIZATION_ALLOWED_ON_START': {
event.stopPropagation();
if (frame.allow === 'monetization') {
const permissionsPolicyPayload = {
allowAttribute: frame.allow,
origin: new URL(frame.src, location.origin).origin,
};
const res = await this.message.send(
'IS_MONETIZATION_ALLOWED',
permissionsPolicyPayload,
);
if (res.success && res.payload) {
this.frames.set(frame, {
frameId: id,
requestIds: payload.map((p) => p.requestId),
...permissionsPolicyPayload,
});
eventSource.postMessage(
{ message: 'START_MONETIZATION', id, payload },
Expand All @@ -218,20 +248,31 @@ export class FrameManager {
}

return;
}

case 'IS_MONETIZATION_ALLOWED_ON_RESUME':
case 'IS_MONETIZATION_ALLOWED_ON_RESUME': {
event.stopPropagation();
if (frame.allow === 'monetization') {
const permissionsPolicyPayload = {
allowAttribute: frame.allow,
origin: new URL(frame.src, location.origin).origin,
};
const res = await this.message.send(
'IS_MONETIZATION_ALLOWED',
permissionsPolicyPayload,
);
if (res.success && res.payload) {
this.frames.set(frame, {
frameId: id,
requestIds: payload.map((p) => p.requestId),
...permissionsPolicyPayload,
});
eventSource.postMessage(
{ message: 'RESUME_MONETIZATION', id, payload },
'*',
);
}
return;
}

default:
return;
Expand Down
9 changes: 9 additions & 0 deletions src/shared/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ export type StopMonetizationPayload = StopMonetizationPayloadEntry[];

export type ResumeMonetizationPayload = StartMonetizationPayload;

export interface IsMonetizationAllowedPayload {
allowAttribute: string;
origin: string;
}

export interface IsTabMonetizedPayload {
value: boolean;
}
Expand All @@ -218,6 +223,10 @@ export type ContentToBackgroundMessage = {
input: ResumeMonetizationPayload;
output: never;
};
IS_MONETIZATION_ALLOWED: {
input: IsMonetizationAllowedPayload;
output: boolean;
};
};
// #endregion

Expand Down