diff --git a/integration-libs/opf/base/root/model/opf-base.model.ts b/integration-libs/opf/base/root/model/opf-base.model.ts index d8a0337608a..3bcdf20d973 100644 --- a/integration-libs/opf/base/root/model/opf-base.model.ts +++ b/integration-libs/opf/base/root/model/opf-base.model.ts @@ -42,6 +42,7 @@ export interface OpfDynamicScript { cssUrls?: OpfDynamicScriptResource[]; jsUrls?: OpfDynamicScriptResource[]; html?: string; + dynamicContext?: string; } export interface OpfKeyValueMap { diff --git a/integration-libs/opf/base/root/services/opf-resource-loader.service.ts b/integration-libs/opf/base/root/services/opf-resource-loader.service.ts index 52f3ed3200a..1476f20fc84 100644 --- a/integration-libs/opf/base/root/services/opf-resource-loader.service.ts +++ b/integration-libs/opf/base/root/services/opf-resource-loader.service.ts @@ -155,7 +155,7 @@ export class OpfResourceLoaderService { }); } - executeScriptFromHtml(html: string | undefined) { + executeScriptFromHtml(html: string | undefined, dynamicContext?: string) { // SSR mode not supported for security concerns if (!isPlatformServer(this.platformId) && html) { const element = new DOMParser().parseFromString(html, 'text/html'); @@ -163,10 +163,111 @@ export class OpfResourceLoaderService { if (!script?.[0]?.innerText) { return; } - Function(script[0].innerText)(); + + const originalScript = script[0].innerText; + const sessionId = this.generateSessionId(); + + let wrappedScript: string; + + if (dynamicContext) { + try { + const contextData = JSON.parse(dynamicContext); + wrappedScript = this.createSessionScopedScript( + originalScript, + contextData, + sessionId + ); + } catch (error) { + console.warn('Failed to parse dynamic context:', error); + wrappedScript = this.createSecureScript(originalScript, sessionId); + } + } else { + wrappedScript = this.createSecureScript(originalScript, sessionId); + } + + this.executeScriptWithSession(wrappedScript, sessionId); } } + /** + * Creates session-scoped script with isolated context + * PSP scripts can use OpfContext.orderId, OpfContext.amount, etc. without modifications + */ + private createSessionScopedScript( + originalScript: string, + contextData: any, + sessionId: string + ): string { + return ` + (function() { + 'use strict'; + + // Session-isolated context (no global pollution) + const OpfContext = ${JSON.stringify(contextData)}; + const SessionId = '${sessionId}'; + + // Context is immediately available - no async waiting + console.log('PSP: Session', SessionId, 'Context:', OpfContext); + + // Original PSP script runs with context available immediately + ${originalScript} + + })(); + `; + } + + /** + * Creates secure script without context but with session ID + */ + private createSecureScript( + originalScript: string, + sessionId: string + ): string { + return ` + (function() { + 'use strict'; + + const OpfContext = {}; + const SessionId = '${sessionId}'; + + console.log('PSP: Session', SessionId, 'No context available'); + + ${originalScript} + + })(); + `; + } + + /** + * Generate unique session ID + */ + private generateSessionId(): string { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2); + return `opf-session-${timestamp}-${random}`; + } + + /** + * Execute script with session isolation + */ + private executeScriptWithSession( + scriptContent: string, + sessionId: string + ): void { + const scriptElement = this.document.createElement('script'); + scriptElement.textContent = scriptContent; + scriptElement.setAttribute('data-session-id', sessionId); + scriptElement.setAttribute('data-opf-script', 'true'); + this.document.head.appendChild(scriptElement); + + // Clean up after execution + setTimeout(() => { + if (scriptElement.parentNode) { + scriptElement.parentNode.removeChild(scriptElement); + } + }, 100); + } + clearAllResources() { this.document .querySelectorAll(`[${this.OPF_RESOURCE_ATTRIBUTE_KEY}]`) diff --git a/integration-libs/opf/checkout/components/opf-checkout-payment-wrapper/opf-checkout-payment-wrapper.service.ts b/integration-libs/opf/checkout/components/opf-checkout-payment-wrapper/opf-checkout-payment-wrapper.service.ts index 82864668529..03b01dc2df1 100644 --- a/integration-libs/opf/checkout/components/opf-checkout-payment-wrapper/opf-checkout-payment-wrapper.service.ts +++ b/integration-libs/opf/checkout/components/opf-checkout-payment-wrapper/opf-checkout-payment-wrapper.service.ts @@ -66,7 +66,7 @@ export class OpfCheckoutPaymentWrapperService { isError: false, }); - protected executeScriptFromHtml(html: string): void { + protected executeScriptFromHtml(html: string, dynamicContext?: string): void { /** * Verify first if customer is still on the payment and review page. * Then execute script extracted from HTML to render payment provider gateway. @@ -82,7 +82,10 @@ export class OpfCheckoutPaymentWrapperService { ) .subscribe(() => { setTimeout(() => { - this.opfResourceLoaderService.executeScriptFromHtml(html); + this.opfResourceLoaderService.executeScriptFromHtml( + html, + dynamicContext + ); }); }); } @@ -172,7 +175,9 @@ export class OpfCheckoutPaymentWrapperService { renderPaymentGateway(config: OpfPaymentSessionData) { if (config?.dynamicScript) { const html = config?.dynamicScript?.html; - + let dynamicContext = config?.dynamicScript?.dynamicContext; + dynamicContext = + '{"orderId":"1234","amount":30,"currency":"USD","billingAddress":{"addressLine1":"123 Main St","city":"New York","country":"US"}}'; const paymentOptionId = this.lastPaymentOptionId; this.opfResourceLoaderService @@ -190,7 +195,7 @@ export class OpfCheckoutPaymentWrapperService { }); if (html) { - this.executeScriptFromHtml(html); + this.executeScriptFromHtml(html, dynamicContext); } }) .catch(() => { diff --git a/integration-libs/opf/examples/context-injection-example.md b/integration-libs/opf/examples/context-injection-example.md new file mode 100644 index 00000000000..8ab63834ce6 --- /dev/null +++ b/integration-libs/opf/examples/context-injection-example.md @@ -0,0 +1,212 @@ +# OPF Context Injection Implementation + +## Overview + +This implementation provides a **session-scoped context injection** solution that allows PSP scripts to access backend context data without modifications. PSP scripts can simply use `OpfContext.orderId`, `OpfContext.amount`, etc. + + +## Implementation + +### 1. Spartacus Context Injection + +```typescript +// OpfResourceLoaderService +executeScriptFromHtml(html: string | undefined, dynamicContext?: string): void { + if (!isPlatformServer(this.platformId) && html) { + const element = new DOMParser().parseFromString(html, 'text/html'); + const script = element.getElementsByTagName('script'); + + if (!script?.[0]?.innerText) { + return; + } + + const originalScript = script[0].innerText; + const sessionId = this.generateSessionId(); + + let wrappedScript: string; + + if (dynamicContext) { + try { + const contextData = JSON.parse(dynamicContext); + wrappedScript = this.createSessionScopedScript(originalScript, contextData, sessionId); + } catch (error) { + console.warn('Failed to parse dynamic context:', error); + wrappedScript = this.createSecureScript(originalScript, sessionId); + } + } else { + wrappedScript = this.createSecureScript(originalScript, sessionId); + } + + this.executeScriptWithSession(wrappedScript, sessionId); + } +} +``` + +### 2. Session-Scoped Script Creation + +```typescript +private createSessionScopedScript(originalScript: string, contextData: any, sessionId: string): string { + return ` + (function() { + 'use strict'; + + // Session-isolated context (no global pollution) + const OpfContext = ${JSON.stringify(contextData)}; + const SessionId = '${sessionId}'; + + // Context is immediately available - no async waiting + console.log('PSP: Session', SessionId, 'Context:', OpfContext); + + // Original PSP script runs with context available immediately + ${originalScript} + + })(); + `; +} +``` + +### 3. PSP Script Usage + +```javascript +// PSP Script - No modifications needed! +(function() { + 'use strict'; + + // Context is immediately available + console.log('PSP: Order ID:', OpfContext.orderId); + console.log('PSP: Amount:', OpfContext.amount); + console.log('PSP: Session:', SessionId); + + // Use context data for PSP operations + const paymentData = { + orderId: OpfContext.orderId, + amount: OpfContext.amount || 0, + currency: OpfContext.currency || 'USD', + billingAddress: OpfContext.billingAddress + }; + + // Initialize PSP with context data + initializePaymentForm(paymentData); + +})(); +``` + +## Complete Flow Example + +### 1. OPF Backend Response + +```json +{ + "dynamicScript": { + "dynamicContext": "{\"orderId\":\"12345\",\"billingAddress\":{\"addressLine1\":\"test street\"},\"amount\":99.99,\"currency\":\"USD\"}", + "html": "" + } +} +``` + +### 2. Spartacus Processing + +```typescript +// 1. Parse context +const contextData = JSON.parse(dynamicContext); +// contextData = { orderId: "12345", billingAddress: {...}, amount: 99.99, currency: "USD" } + +// 2. Generate session ID +const sessionId = "opf-session-1703123456789-abc123"; + +// 3. Wrap PSP script with context +const wrappedScript = ` + (function() { + 'use strict'; + + const OpfContext = ${JSON.stringify(contextData)}; + const SessionId = '${sessionId}'; + + // Original PSP script content + // ... PSP script code ... + + })(); +`; + +// 4. Execute wrapped script +executeScriptWithSession(wrappedScript, sessionId); +``` + +### 3. PSP Script Execution + +```javascript +// PSP script runs with context immediately available +console.log('PSP: Session', SessionId); // "opf-session-1703123456789-abc123" +console.log('PSP: Order ID:', OpfContext.orderId); // "12345" +console.log('PSP: Amount:', OpfContext.amount); // 99.99 + +// No async waiting, no event listeners needed +initializePaymentForm(); +``` + +## CSP Compliance + +### With Nonce Support + +```typescript +// CSP-compliant version with nonce +private createNonceSecuredScript(originalScript: string, contextData: any, nonce: string, sessionId: string): string { + return ` + (function() { + 'use strict'; + + // Validate nonce for security + if (!'${nonce}' || '${nonce}'.length < 16) { + throw new Error('Invalid nonce provided'); + } + + // Session-isolated context + const OpfContext = ${JSON.stringify(contextData)}; + const SessionId = '${sessionId}'; + const Nonce = '${nonce}'; + + // Original PSP script runs with context available immediately + ${originalScript} + + })(); + `; +} +``` + +### CSP Headers + +```http +Content-Security-Policy: + script-src 'self' 'nonce-{random-nonce}' https://trusted-psp.com; + object-src 'none'; + base-uri 'self'; + default-src 'self'; +``` + +## Usage + +PSP scripts can now simply use: + +```javascript +// Access order information +const orderId = OpfContext.orderId; +const amount = OpfContext.amount; +const currency = OpfContext.currency; + +// Access billing information +const billingAddress = OpfContext.billingAddress; +const customerEmail = OpfContext.customerEmail; + +// Access session information +const sessionId = SessionId; + +// Use in PSP operations +const paymentData = { + orderId: OpfContext.orderId, + amount: OpfContext.amount, + currency: OpfContext.currency, + billingAddress: OpfContext.billingAddress +}; +``` + +This approach provides the **easiest integration** for PSP customers while maintaining **maximum security** and **CSP compliance**.