pleat
v0.9.1
Published
<h3>A Framework for Micro Clients and Sandboxed Scripts</h3>
Downloads
6
Readme
Pleat
- Tiny Library (
2kbbrotli compressed) - No external dependencies
- Handles iframe resizing
- RPC and
ProxyAPI makes it SRI safe - Fully sandboxed
- Supports cross-origin and same-origin iframes
- Self hostable
- Open source
Basics
Pleat facilitates the integration between a Client and a Widget or Service.
Client
The Client is the website that wants to use a Widget or Service.
Examples of a Client are a website that wants to use:
- A "syntax highlighting"
Widgetfor their blog site - A payment processing
Widget - A pop-over login page
Widgetoffered by a managed authentication provider
Widget
A Widget is a micro client loaded as an iframe. Pleat aims to make Widgets function like pseudo "Web Components" that can be consumed as dynamic third party sources without exposing the Client or Widget provider to the security vulnerabilities of running dynamic scripts on the root Document context.
This is useful for cases where the Widget wants to be isolated from the Client for security reasons (example payment processors or authentication login pages) or where the Client wants to be able to use a UI component that requires access to first party local storage and HTTP APIs (like a chat client or "comments section" service)
Service
A Service is a sandboxed script that can be launched by the Client and any Widget. A Service does not have access to the Window/Document of the Client/Widget though it's able to export a programmatic API (method).
This is useful for scenarios where a Client wants to use a dynamic third party script without exposing their site to vulnerabilities and allows for use cases where functionality/state needs to be shared between Widgets.
The exported API is implemented in the consumer using an RPC protocol and JavaScript Proxy objects wrappers. This allows the consumer to lock the loaded client script using an SRI hash while still allowing the Service provider to dynamically update their service, expanding functionality.
Depending on the requirements of the service, services can be spawned as web workers, hidden iframes or wasm modules.
Usage
The Client/Consumer
The Client initializes a Widget using the createWidget method which returns back a Widget instance they can interact with.
<html>
<body>
<h1>Hello and Welcome to my Site!</h1>
<script type = module>
import Pleat from 'pleat/client'
// Start the pleat engine and create a Widget
const yourWidget = Pleat.createWidget({
url: 'https://your-iframe.com/iframe.html'
})
// Adds the iframe to the body
yourWidget.appendTo({ selector: 'body' })
// The Client can run methods registered in the Widget
const result = await yourWidget.foo()
console.log(result) // "Hi from foo"
</script>
</body>
</html>Widgets
The Widget is a normal iframe that registers itself using the Pleat Widget API.
From within the iframe
<html>
<body>
<h1>Hello World Widget!</h1>
<button>Close!</button>
<script type = module>
import Pleat from 'pleat/widget'
// The Widget API
const widget = Pleat.initWidget({
// Automatically resize iframe when contents change
autoResize: true,
})
// Register a method for the Client
widget.registerMethod('foo', () => {
return "Hi from foo"
})
document.querySelector('button').onclick = () => {
// Close the widget
widget.close()
}
// Widget will show up at this point
widget.ready()
</script>
</body>
</html>Self Hosting
To tie the entities together, Pleat uses a central router called the Engine. This is an invisible iframe used to coordinate actions of Widgets and Services, manage child Widgets.
A self hosted Engine can extend the Client and Widget APIs for a specific use case (e.g. a payment processor).
The engine must be hosted on a domain, for instance: https://example.com/pleat/engine/index.html. Below is an example of an Engine that registers methods on the Client.
<html>
<body>
<script type = module>
import Pleat from 'pleat/engine'
// The Engine API
const engine = Pleat.initEngine()
// Register a method for the Client
engine.registerClientMethod('initChat', async ({ accountSecret }) => {
// Maybe make a request to your back end to create a chat session
const response = await fetch(`https://example.com/api/start-chat?secret=${accountSecret}`).then(r => r.json())
// Create a widget on behalf of the Client with a chat window
const widget = engine.createWidget({
url: 'https://example.com/pleat/widget/chat/index.html',
config: response
})
// Append chat Widget to the Client "body"
widget.appendTo('body')
})
// Engine will begin communicating with the Client at this pont
engine.connect()
</script>
</body>
</html>Where the Client would consume your self hosted Engine like
<html>
<body>
<h1>Hello and Welcome to my Site!</h1>
<script type = module>
import Pleat from 'pleat/client'
// The Engine API
const engine = Pleat.init({
engineUrl: 'https://example.com/pleat/engine/index.html'
})
// Will run the "initChat" method specified by the Engine
await engine.initChat({
accountSecret: '1234567890'
})
</script>
</body>
</html>Interfaces
Client
export interface IClientBase {
// Create a Widget
createWidget<T = DefaultWidget>(options: CreateWidgetOptions): IWidget<T>
// Communicate to Engine
on<T = unknown>(eventName: string, callback: EventCallback<T>): DisposeFunc
runMethod(methodName: string, args: any[]): Promise<any>
// Close all widgets and shutdown Pleat
shutdown(): void
}Widget
export interface IWidgetBase {
// Begin sending/receiving messages
connect(): void
// Communicate to Parent
emit(eventName: string, data?: any): void
registerMethod<T extends any[], U = any>(methodName: string, action: WidgetMethod<T, U>): void
// Communicate to Client
emitClient(eventName: string, data?: any): void
// Set external styles of iframe
putStyles(styles: CSSStyles): void
// Create nested Widget
createChildWidget<T = DefaultWidget>(options: CreateWidgetOptions): IChildWidget<T>
// Run method exposed by engine
runMethod(methodName: string, args: any[]): Promise<any>
on<T = unknown>(eventName: string, callback: EventCallback<T>): DisposeFunc
// Close Widget
close(): void
}Engine
export interface IEngine {
// Create a Widget on the Client controlled by the Engine
createWidget<T = DefaultWidget>(options: CreateWidgetOptions): IWidget<T>
// Communicate to Client
emitClient(eventName: string, data?: any): void
registerClientMethod<T extends any[], U = any>(methodName: string[], action: RegisteredMethod<T, U>): void
// Communicate to Widgets
emitWidgets(eventName: string, data?: any): void
registerWidgetMethod<T extends any[], U = any>(methodName: string[], action: RegisteredMethod<T, U>): void
// Close all widgets and shutdown Pleat
shutdown(): void
}