[Table of contents](README.md#table-of-contents) # Intents - [Overview](#overview) - [Glossary](#glossary) - [Proposal](#proposal) - [1. Manifest](#1-manifest) - [2. Intent Start](##2-intent-start) - [3. Service Resolution](#3-service-resolution) - [4. Handshake](#4-handshake) - [5. Processing & Terminating](#5-processing--terminating) - [Routes](#routes) - [Annexes](#annexes) - [Use cases](#use-cases) - [Bibliography & Prior Art](#bibliography--prior-art) - [Discarded Ideas](#discarded-ideas) ## Overview A typical Twake Workplace runs multiple applications, but most of these applications are focused on one task and interact with one particular type of data. However, Twake Workplace especially shines when data is combined across apps to provide an integrated experience. This is made difficult by the fact that apps have no dedicated back-end and that they have restricted access to documents. This document outlines a proposal for apps to rely on each other to accomplish certain tasks and gain access to new documents, in a way that hopefully is neither painful for the users nor the developers. ## Glossary - **Intent**: Intents, sometimes also called Activities, is a pattern used in environments where multiple apps with different purposes coexist. The idea is that any app can express the need to do _something_ that it can't do itself, and an app that _can_ do it will take over from there. - **Stack**: refers to [cozy-stack](https://github.com/cozy/cozy-stack/), the server-side part of the Cozy infrastructure. - **Client**: the client is the application that _starts_ an intent. - **Service**: the service is the application that _handles_ an intent started by a client. - **Available apps**: Uninstalled applications that _handles_ the intent. ## Proposal In this proposal, services declare themselves through their manifest. When a clients starts an intent, the stack finds the appropriate service and help the two apps to create a communication channel between them. After the channel is ready, the intent is processed. These steps are detailed below. ### 1. Manifest Every app can register itself as a potential handler for one or more intents. To do so, it must provide the following information for each intent it wishes to handle: - `action`: A verb that describes what the service should do. The most common actions are `CREATE`, `EDIT`, `OPEN`, `PICK`, and `SHARE`. While we recommend using one of these verbs, the list is not exhaustive and may be extended by any app. - `type`: One or more types of data on which the service knows how to operate the `action`. A `type` can be expressed as a [MIME type](https://en.wikipedia.org/wiki/Media_type) or a [Cozy Document Type](https://github.com/cozy/cozy-stack/blob/master/docs/data-system.md#typing). The application must have permissions for any Cozy Document Type listed here. You can also think of the `type` as the intent's subject. - `href`: the relative URL of the route designed to handle this intent. A query-string with the intent id will be added to this URL. These informations must be provided in the manifest of the application, inside the `intents` key. Here is a very simple example: ```json "intents": [ { "action": "PICK", "type": ["io.cozy.files"], "href": "/pick" } ] ``` When this service is called, it will load the page `https://service.domain.example.com/pick?intent=123abc`. Here is an example of an app that supports multiple data types: ```json "intents": [ { "action": "PICK", "type": ["io.cozy.files", "image/*"], "href": "/pick" } ] ``` Finally, here is an example of an app that supports several intent types: ```json "intents": [ { "action": "PICK", "type": ["io.cozy.files", "image/*"], "href": "/pick" }, { "action": "VIEW", "type": ["image/gif"], "href": "/viewer" } ] ``` This information is stored by the stack when the application is installed. ### 2. Intent Start Any app can start a new intent whenever it wants. When it does, the app becomes the _client_. To start an intent, it must specify the following information: - `action`: an action verb, which will be matched against the actions declared in services manifest files. - `type`: a **single** data type, which will be matched against the types declared in services manifest files. There are also two optional fields that can be defined at the start of the intent: - `data`: Any data that the client wants to make available to the service. `data` must be a JSON object but its structure is left to the discretion of the client. The only exception is when `type` is a MIME type and the client wishes to send a file to the service. In that case, the file should be represented as a base-64 encoded [Data URL](http://dataurl.net/#about) and must be named `content`. This convention is also recomended when dealing with other intent `type`s. See the examples below for an example of this. - `permissions`: When `type` is a Cozy Document Type and the client expects to receive one or more documents as part of the reply from the service, the `permissions` field allows the client to request permissions for these documents. `permissions` is a list of HTTP Verbs. Refer [to this section](https://github.com/cozy/cozy-stack/blob/master/docs/permissions.md#verbs) of the permission documentation for more information. **Note**: if the intent's subject is a Cozy Doctype that holds references to other Cozy Documents (such as an album referencing photos or a playlist referencing music files), the permissions should be granted for the referenced documents too, whenever possible. Here is an example of what the API could look like: ```js // "Let the user pick a file" cozy.intents.start('PICK', 'io.cozy.files') .then(document => { // document is a JSON representation of the picked file }); // "Create a contact, with some information already filled out" cozy.intents.start('CREATE', 'io.cozy.contacts', { name: 'John Johnsons', tel: '+12345678' email: 'john@johnsons.com' }) .then(document => { // document is a JSON representation of the contact that was created }); // "Save this file somewhere" cozy.intents.start('CREATE', 'io.cozy.files', { content: 'data:application/zip;base64,UEsDB...', name: 'photos.zip' }); // "Create a new note, and give me read-only access to it" cozy.intents.start('CREATE', 'io.cozy.notes', {}, ['GET']) .then(document => { // document is a JSON representation of the note that was created. // Additionally, this note can now be retrieved through the API since we have read access on it. }); // "Create an event based on the provided data, and give me full access to it" cozy.intents.start('CREATE', 'io.cozy.events', { date: '2017/06/24', title: 'Beach day' }, ['ALL']) .then(document => { // document is a JSON representation of the note that was created. // Additionally, this note can now be retrieved through the API since we have read access on it. }); // "Crop this picture" cozy.intents.start('EDIT', 'image/png', { content: 'data:image/png;base64,iVBORw...', width: 50, height: 50 }) .then(image => { //image is the edited version of the image provided above. }) ``` ### 3. Service Resolution The service resolution is the phase where a service is chosen to handle an intent. This phase is handled by the stack. After the client has started it's intent, it sends the `action`, the `type` and the `permissions` to the stack. The stack will then traverse the list of installed apps and find all the apps that can handle that specific combination. Note that if the intent request `GET` permissions on a certain Cozy Document Type, but the service does not have this permission itself, it can not handle the intent. The stack then stores the information for that intent: - A unique id for that intent - Client URL - Service URL - `action` - `type` - `permissions` Finally, the service URL is suffixed with `?intent=` followed by the intent's id, and then sent to the client. The service URL opens the app's frontend page that handles the intent. By default, this page is on the app's cozy subdomain. To host it elsewhere, the app declares `service_url_flag` in its manifest. This names a feature flag containing the external base URL, optionally with a path prefix. The stack appends the intent's `href` and the intent ID. For example, a Calendar manifest with `"service_url_flag": "calendar_service_url"` and an intent whose `href` is `/open-event`, with that flag set to `https://calendar.example.com`, produces: ```text https://calendar.example.com/open-event?intent=abc ``` The calling app opens this page in an iframe. Calendar must implement the `/open-event` page, which retrieves the intent details from the stack's `GET /intents/abc` API using an OAuth token linked to Calendar (`software_id: registry://calendar`). The frontend route can have any name; the `/intents` API remains on the stack. Only `http` and `https` URLs with an ASCII host (letters, digits, dots and dashes, with an optional port) and no userinfo are accepted. Missing or invalid flag values, and values pointing to the app's cozy host, fall back to the cozy subdomain. For valid external URLs, the stack drops the query and fragment from the base URL while keeping its host and path prefix. The stack adds the origin of such external services to the `frame-src` directive of the Content Security Policy of every app, so that any client app can open them. The external host must itself allow the client apps in its `frame-ancestors` directive. The stack cannot set the `frame-ancestors` of a page it does not serve. For a service it serves, it allows the client app only: its client URL and, when it is hosted elsewhere (`client_url_flag`), its cozy subdomain, which can frame it. The intent gives these origins in `frameAncestors`, so that an external service can check who frames it, for example by comparing them with `location.ancestorOrigins` in browsers that have it. They are serialized as browsers serialize origins: host in lower case, internationalized domain names in punycode, no default port. The field is in the answers of `POST /intents` and `GET /intents/:id`, and in the intent inlined in the page of a service the stack serves. It is absent when the client is not a webapp (a konnector, an OAuth client not linked to an app). ### 4. Available apps In addition to the services that manage intents, a list of available (but not installed) applications of the instance that handles the intent is returned. This object list is called `availableApps` and contains: - The application `slug` - The application `name` #### Service choice If more than one service match the intent's criteria, the stack returns the list of all matching service URLs to the client (and stores it in the service URL version of the intent it keeps in memory). The client is then free to pick one arbitrarily. The client may also decide to let the user choose one of the services. To do this, it should start another intent with a `PICK` action and a `io.cozy.apps` `type`. This intent should be resolved by the stack to a special page, in order to avoid having multiple services trying to handle it and ending up in a loop. This special page is a service like any other; it expects the list of services to pick from as input data, and will return the one that has been picked to the client. The client can then proceed with the first intent. The user may decide to abort the intent before picking a service. If that is the case, the choice page will need to inform the client that the intent was aborted. #### No available service If no service is available to handle an intent, the stack returns an error to the client. However, some non-installed applications may handle the intent, and are listed in the `availableApps` field. ### 4. Handshake The next phase consist of the client and the service establishing a communication channel between them. The communication will be done using the [window.postMessage](https://developer.mozilla.org/fr/docs/Web/API/Window/postMessage) API. It is documented with more details in [https://docs.cozy.io/en/cozy-interapp/](cozy-interapp documentation) but you can find the basic operation below. #### Service Initialization When the client receives the service URL from the stack, it starts to listen for messages coming from that URL. Once the listener is started, it opens an iframe that points to the service's URL. Some permissions must always be allowed on the iframe, whatever the intent. Today, it is `clipboard-write` (`