Supervisor
The supervisor is the name of the client-side code that is run by a psibase app to mediate communication between apps and provide certain services to apps. All plugins run within the supervisor, and can interact with each other via message passing through the supervisor. The supervisor also provides core functionality to each psibase app, such as transaction packing, transaction submission, acquiring user authorization, resource management, and more.
Capabilities
Supervisor is responsible for:
- Client side peering - For exchange of data using WebRTC
- Event subscription feeds - Allowing UIs to respond to server-side events
- Communication with plugins - For modularizing app functionality
- Transaction signing and authorization (Smart authorization)
- Packing transactions into the
fracpack
binary format
Client-side peering
➕ TODO: Document client-side WebRTC-based peering capabilities facilitated by Supervisor.
Event subscription feeds
Psibase apps may be interested in reacting to the emission of an events from a service. In psibase networks, it is the supervisor that is responsible for providing an interface that allows a psibase app to subscribe to notifications for certain events.
The supervisor will poll the root domain to watch for events that have been subscribed to by any psibase apps. When an event occurs, it will notify the psibase app using an "Event"
message.
Note: These events are polled at a particular frequency, such as once per second. This notification system is therefore not designed to benefit apps which have hard real time event notification requirements. For such requirements it is recommended to run your own infrastructure provider node.
Communication with plugins
Inter-app communication via plugins at 3 levels of detail.
First, here's what the frontend code looks like for an app to call method method_name
on app app2
:
const supervisor = new Supervisor();
const res = await supervisor.call({
app: "app2",
method: "method_name",
params: { ... },
})
Proposed Supervisor API
const services = [{
service: "sample_srvc_name",
}, {
service: "srvc_name2",
plugin: "plugin2" // optional; assumed to be simply "plugin" by default
}
];
// Supervisor.connect(...) will internally create a uuid to represent the client session
const supervisor = await Supervisor.connect(options: {
services // optional: for, e.g., preloading. the <wasm filename>.wasm
// if not provided, Supervisor will load the wasm specified in supervisor.call()
});
const res = await supervisor.call({
service: "service_name",
plugin: "plugin_name", // optional
method: "method_name",
params: {
arg1: "arg value 1"
}
});
App1's call is routed through the Supervisor, which then routes the call to the intended app. This hierarchy of apps ensures all apps can verify the source/destination of their function calls is the Supervisor, which provides a security guaranty.
When App1 instantiates the Supervisor, Supervisor is embedded in a hidden iframe.
The Supervisor, in turn, embeds another iframe that contains app2
's plugin.
Finally, Supervisor mediates the call from app1 to app2's plugin (as well as a return value).
For example, if app1.psibase
is requested from a psibase infrastructure provider, then the element stored at the root path in that service is returned to the client. That UI element is the root page of the psibase app, which will then request the supervisor from its domain on the server. After the supervisor loads, the psibase app can call into the supervisor in order to execute functionality defined in app2's plugin.
⚠️ For security, plugins should only listen for messages from the supervisor (identified by its domain). Otherwise, an attacker app could instantiate the victim app within its own iframe and impersonate the supervisor.
Supervisor initialization
Internally, instantiation of the supervisor creates an ID for identifying the window/session (to help distinguish messages to handle the case where the app is open in multiple browser tabs).
TASK: Update below if/as Supervisor API changes
sequenceDiagram %%{init: { 'sequence': {'noteAlign': 'left'} }}%% box rgb(85,85,85) client-side, app1.psibase participant app1 participant plugin1 end box rgb(85,85,85) client-side, supervisor.psibase participant supervisor end box rgb(85,85,85) server-side participant server end app1->>server: new Supervisor() [fetch supervisor.psibase] server-->>app1: <return> note over app1: generate session ID app1->>supervisor: create(sessionID, myplugin: "plugin1.psibase") supervisor->>plugin1: create(sessionID) plugin1-->>supervisor: <return> supervisor-->>app1: <return>
NOTE: plugin location will be assumed to be the default of
Adding actions
A psibase app can both read from and write to its server side state. The process of writing to server-side state is called "submitting a transaction". A transaction is a structured payload that is authorized to execute an action on one or more services on behalf of a user. An action in a service is a callable function that updates the server-side state.
A psibase app must wrap its client-side logic for calling actions into a plugin. On the client-side, apps may reach across the domain boundary using the Window.PostMessage
API. This functionality is used to allow communication between the app, the supervisor, and all plugins. Plugins only need to listen to messages from the supervisor. The supervisor itself listens both to messages from its parent window and also to any of the child windows it creates.
A transaction context is started when an app first calls into a plugin, meaning that actions can now be added into a transaction object by plugins sending action requests to the supervisor. When the initial call into the plugin is complete, the transaction context is closed and the transaction object will not accept any more actions. The transaction object is then authorized and submitted to the server.
sequenceDiagram %%{init: { 'sequence': {'noteAlign': 'left'} }}%% box rgb(85,85,85) client-side, app1.psibase participant app1 participant plugin1 end box rgb(85,85,85) client-side, supervisor.psibase participant supervisor end box rgb(85,85,85) server-side participant server end Note over app1,server: Supervisor initialization app1->>supervisor: call action1@plugin activate supervisor supervisor->>plugin1: call action1@plugin activate plugin1 plugin1->>supervisor: add action1<br>to transaction note over supervisor: transaction {<br>action1<br>} supervisor-->>plugin1: <return> plugin1-->>supervisor: <return> deactivate plugin1 supervisor->>server: submit transaction supervisor-->>app1: <return> deactivate supervisor
Requesting user permission
When a function in one plugin calls a function in another plugin, either function is permitted to add actions into the current transaction object. However, a plugin may not necessarily trust the legitimacy of the call to its plugin if it came from a source other than its own app. Therefore, all plugins have the ability to ask the supervisor to open a pop-up window in order to explicitly confirm that the user is granting permission to the caller app to make this request on their behalf.
When a plugin asks the supervisor to open a popup, it provides a path relative to its own domain at which the UI for a user confirmation dialog can be retrieved. The supervisor then opens this popup in another window on behalf of the plugin.
User experience note: The first time the supervisor requests user authentication, a user will need to enable popups from the supervisor in their browser. This is an unfortunate side-effect of pop-up blockers, but it will only happen once and the only domain that needs this permission is the supervisor domain.
%%{init: { 'sequence': {'noteAlign': 'left'} }}%% sequenceDiagram actor alice box rgb(85,85,85) client-side, app1.psibase participant app participant plugin1 end box rgb(85,85,85) client-side, supervisor.psibase participant supervisor end box rgb(85,85,85) client-side, app2.psibase participant plugin2 participant auth_page end box rgb(85,85,85) server side participant server end Note over app,server: Supervisor initialization app->>supervisor: call action@plugin1 supervisor->>plugin1: call action activate plugin1 plugin1->>supervisor: add action1<br>to transaction activate supervisor note over supervisor: transaction {<br>action1<br>} supervisor->>plugin1: <return> deactivate supervisor plugin1->>supervisor: action@plugin2 activate supervisor supervisor->>plugin2: action activate plugin2 # Interact with the user note over plugin2: I do not trust<br>plugin1 yet plugin2->>supervisor: get auth(/auth) note over supervisor: opens auth page<br>app2.psibase/auth alice->>auth_page: Grant permission note over auth_page: save permission<br>in localstorage note over plugin2: triggered by local<br>storage write plugin2->>supervisor: add action2<br>to transaction note over supervisor: transaction {<br>action1<br>action2<br>} supervisor-->>plugin2: <return> deactivate plugin2 plugin2-->>supervisor: <return> deactivate supervisor supervisor-->>plugin1: deactivate plugin1 deactivate plugin1 supervisor->>server: submits transaction
Message formats
This section is incomplete. More details are needed.
The supervisor listens for messages posted to its window messages with the following payload:
{
"message": {
"session": "number", // A number identifying the session/window on which the supervisor was instantiated
"type": "...", // Where the type is one of the supported supervisor message types
"payload": "..." // Contents depends on the message type
}
}
Supported supervisor message types:
"Action"
- Used when requesting the supervisor add an action to the pending transaction"IAC"
- Used when calling a function defined in a plugin of another application"IACResponse"
- A response to a prior IAC"Event"
- A notification from the supervisor that an event to which your app subscribed has been emitted"TransactionReceipt"
- A response with the payload returned by the node to whom a transaction was submitted"ChangeHistory"
- A notification fired by a psibase app when its history changes, used for synchronizing the URL
Depending on the type of message, the payload is required to contain different parameters and the supervisor will behave differently.
➕ TODO: document supervisor message bodies
Message sequences
Transaction construction
An app's user interface may call an "IAC"
message on a supervisor, which lets plugins request actions to be added to a transaction. A transaction may be packed with multiple actions, which eases the authentication burden on the infrastructure providers who only need to run the authorization code once for the entire set of actions in the transaction, rather than once for each independent transaction.
The supervisor will open a transaction at the start of a call to a plugin. Plugins may send the "Action"
message to the supervisor to attempt to add the action into the transaction. Furthermore, plugins may themselves use the "IAC"
message to call into other plugins. The supervisor will handle packing all the transactions (depth-first) that were requested by each plugin accessed by the entire interaction.
A plugin signals that it is complete by sending the "IACResponse"
message to the supervisor. Once the initial plugin function sends its "IACResponse"
message, the transaction (which may now have many actions added to it) is considered closed to new actions.
🕓 Timeouts: If the supervisor does not receive an
"IACResponse"
message from a plugin in less than 100ms after it is initially called, then it will consider it failed. Caller plugins will be notified of the failure.
Transaction authorization
Once the transaction construction is complete and new actions cannot be added, then it must be authorized using smart authorization and submitted.
Authorizing a transaction is a multi-step process that requires aggregating one or more claims into the transaction, calculating the transaction hash, and then aggregating a proof for each claim (See smart authorization docs for more details). Once the claims and proofs have been added into the transaction object the transaction is ready to be packed into a binary format and sent to an infrastructure provider node.
Transaction packing
Transactions need to be packed into the fracpack
format before they are submitted. To do this, first, actions must be packed. To pack an action, supervisor will expect that the service on whom an action is being called handles requests made to POST /pack_action/x
, wher x
is the name of the action. The message body will include the action arguments, and the service is responsible for returning the packed application/octet-stream
back to the supervisor, which can then include the packed actions in its transaction.
Supervisor must then use endpoints that are exposed by psibase infrastructure provider nodes: POST /common/pack/Transaction
and POST /common/pack/SignedTransaction
. These endpoints are used to pack the transaction, which can then be sent to the node.