pleat
v0.9.1
Published
<h3>A Framework for Micro Clients and Sandboxed Scripts</h3>
Downloads
34
Readme
Pleat
- Tiny Library (
2kb
brotli compressed) - No external dependencies
- Handles iframe resizing
- RPC and
Proxy
API 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"
Widget
for their blog site - A payment processing
Widget
- A pop-over login page
Widget
offered 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
}