Developers
Extensions
This is how tenants extend the product. Modules and calendars and tasks are built.
Two kinds of extension stand side by side:
- Modules are small web apps that their authors host. The client shows their pages in sandboxed frames at fixed places, and the pages talk to the room through the scripting API. A module that needs a server side uses the partner API and webhooks. No module code runs on the product's servers.
- Calendars and tasks are built into the product: a person connects a Google or Microsoft account and sees their meetings and tasks in the room.
Modules
What a module is
A module is a manifest, a JSON document at an https address, and the pages it names. Its author hosts both. schema/module.schema.json defines the manifest, and examples/module is a module to start from.
| Field | Contents |
|---|---|
version | 1, the version of the manifest's format |
id | the module's name for itself, lowercase letters, digits and -, up to 40, unique within a tenant |
name, description, icon | how the administration and the client present it |
release | the author's version of the module |
permissions | the parts of the scripting API its pages may call, below |
panel | a page that opens in the side panel, with its title and width |
toolbar | up to 4 buttons for the bottom bar or its apps menu, each with a label and an icon, opening the panel or a page in a dialog |
menu | up to 4 entries of the account menu, each opening the panel or a page in a dialog |
areas | up to 10 kinds of map area the module adds, below |
server | the partner API scopes and webhook topics its server side needs, if it has one |
Addresses in the manifest are absolute, or relative to the manifest's own address, and must use https; a development server takes http too. A manifest is at most 64 KiB, and a tenant has at most 20 modules.
Pages
Every page of a module loads in an iframe sandboxed as the websites of map areas are, from the module's own origin, so it reads nothing of the room's page and cannot navigate it. The client adds the room's origin to the page's address in the query parameter world, from which the page loads the scripting API, <world>/scripting/v1/world.js; its calls then go through the API's messages (Scripting API). world.module tells a page which module it belongs to and, for an area page, the area's settings. examples/module/world.js loads the API this way.
Permissions
A module asks for permissions by the API's namespaces, and its pages may call only what it was granted; anything else fails with forbidden.
| Permission | Members of world |
|---|---|
room | room, players and the areas of room, read only |
player | player: moving, status, speech and outline |
state | state and player.state |
chat | chat |
ui | ui: popups, banners, overlays and menus of its own |
controls | controls |
sound | sound and camera |
nav | nav: going to other rooms and opening tabs |
meetings | meetings and spaces |
events | events |
Map scripts keep the whole API, as their map's authors are the tenant's own.
Errors
The server checks a manifest against schema/module.schema.json and answers what is wrong as the map format does, with a code for each rule (Map format):
| Code | Checked by | Rule |
|---|---|---|
module.document | schema | the manifest misses version, id, name or permissions, or has a field the format does not know |
module.id | schema | an id that is not lowercase letters, digits and dashes, up to 40 |
module.label | schema | a name, title or label that is empty or longer than 60 characters |
module.page-url | schema | an address that is neither http(s) nor relative |
module.permission | schema | a permission the API does not know |
module.panel | schema | a panel without its address or title, or of a width outside 20 to 100 percent |
module.opens | schema | a button or an entry that opens neither the panel nor a page with a title |
module.button | schema | a toolbar button without its id, label or what it opens |
module.entry | schema | a menu entry without its id, label or what it opens |
module.area | schema | an area kind without its type, name or page |
module.server | schema | an unknown scope or topic, or topics without the webhook that hears them |
module.address | fetch | an address that does not use https outside development, or that the server cannot fetch |
module.size | fetch | a manifest over 64 KiB, or one that is not JSON |
module.taken | adding | the tenant has a module of that id already |
module.count | adding | the tenant has 20 modules already |
Adding a module
A tenant's admins add a module in the administration by the address of its manifest. The server fetches the manifest, and the admins see its name, icon, places, areas and the permissions it asks for. Approving adds the module, with those permissions, to the tenant. A module is then on or off in the tenant, its worlds and its rooms, as a flag is (Tenancy), and on nowhere until an admin turns it on.
The server fetches each manifest again once a day. A new version that asks for no more than was granted replaces the old one. One that asks for more keeps the old version working until an admin approves the new one.
A module with a server side lists, in server, the partner API scopes and webhook topics it needs. Approving it creates an API key with those scopes and a webhook for those topics in the tenant, named after the module. The administration shows the key and the webhook's signing secret once, for the admin to hand to whoever runs the module's server, as for any partner integration. Removing the module revokes both.
The platform offers modules only where VWS_FEATURES names modules, which it does not by default (Configuration).
The administration's Modules page lists the tenant's modules: for each, its name, release, places and permissions, a newer version waiting for approval, and Remove. Add a module asks for the address, shows what the manifest asks for, and adds it. The endpoints are GET and POST /api/v1/admin/modules, POST /api/v1/admin/modules/preview, POST /api/v1/admin/modules/{module}/approve and DELETE /api/v1/admin/modules/{module}; a refused manifest answers 422 with its issues, in the shape of a refused map package. The settings of the tenant, each world and each room turn modules on, off or forbid them by their ids, under modules (Tenancy), and GET /api/v1/resolve lists the manifests of the modules a room turns on, which the client shows.
Map areas
A module may add kinds of map area. Each declares a type, a name, a JSON Schema of its settings and the page to open. In rooms where the module is on, the map editor offers the area kind among the behaviours as <module id>/<type>, with a form drawn from the schema. A player who enters such an area opens the module's page in the side panel, as a website area opens its page, and the page reads the settings from world.module.
The layout stores the behaviour with the module's id, its type and the settings. In a room where the module is off, the area does nothing. The editor checks the settings against the schema, but the server checks only that they are a JSON object of at most 16 KiB, since a module's schema can change.
Data
Modules keep their data on their own servers. In the room they may use room and player variables through the API, within their state permission.
Calendars and tasks
Calendar and task sync is part of the product, for Google and Microsoft accounts. The product only reads calendars: it never writes to them.
- Connecting. From the account menu, a registered account connects a Google or Microsoft account through OAuth 2.0 with PKCE and offline access, reading calendars and tasks only. The product keeps the token in
calendar_connection, a tenant-scoped table, sealed withVWS_ENCRYPTION_KEY(AES-256-GCM), one connection per account and provider, and the person can disconnect at any time, which deletes it. Google is also asked to revoke the token; Microsoft offers no revocation of one grant's tokens, which expire unused after 90 days. - Operators. Operators register one OAuth client with Google and one with Microsoft for the platform, through
VWS_GOOGLE_CLIENT_IDandVWS_MICROSOFT_CLIENT_IDwith their secrets (Configuration, Operations), whose redirect URI is the platform's public URL with/api/v1/calendars/callback. Tenants switch the feature with the flagcalendars, on by default where the platform offers calendars (Tenancy). - In the room. A panel shows the person's meetings of the day and the tasks due today. A meeting whose location or description holds an address of one of the tenant's rooms offers Go to the room. Five minutes before a meeting, the person is reminded, as an event they can turn off (Identity).
- Scheduled meetings. The product's scheduled meetings offer an
.icsfile that adds them to any calendar: members download it from the room's meeting list, guests from the page of the meeting's link, and every invitation email carries it.
Connecting
The platform has one OAuth client per provider and so one redirect URI, on its public URL, for every tenant. A connection therefore goes through four steps, so that a calendar is connected only to the account that asked for it:
POST /api/v1/me/calendarswith the provider and the path of the page to come back to keeps a grant in Redis for ten minutes (the tenant, the account, the provider, the PKCE verifier and the page) under a randomstate, and answers the provider's authorization address, where the browser goes.- The provider sends the browser to
GET /api/v1/calendars/callbackon the public URL, which takes the grant once, exchanges the code with the verifier, reads the address of the account at the provider and keeps the sealed token in Redis for ten minutes under a random claim. - The callback sends the browser back to the page on the tenant's host with the query parameter
calendar, the claim, orcalendarError,deniedwhen the person refused andfailedwhen the exchange failed. - The client takes the claim out of the address and sends it with
POST /api/v1/me/calendars/claim, with its own access token. Only the account that started the grant connects the calendar; any other account's claim is refused and spent, so that a link to a provider's consent page that someone else started connects nothing to their account.
GET /api/v1/me/calendars lists the providers the account may connect, none while the flag is off or for an account that is not registered, and its connections, which stay listed while the flag is off so that they can still be removed.
| Provider | Scopes | Reads |
|---|---|---|
openid, email, calendar.events.readonly, tasks.readonly | the primary calendar's events (Calendar API, single events in order), the open tasks of every task list due on the day (Tasks API) | |
| Microsoft | openid, email, offline_access, User.Read, Calendars.Read, Tasks.Read | the default calendar's view of the day in UTC, the open tasks of every To Do list due on the day (Graph); the common endpoints admit work, school and personal accounts |
Google is asked for consent each time, as it hands out a refresh token only then. A token is refreshed when it expires and kept sealed again, with the new refresh token Microsoft hands out with each refresh. A connection whose provider refuses its refresh token (invalid_grant) turns expired and is no longer read until the person connects again, which renews it.
The day
GET /api/v1/me/agenda takes the person's day as the client sees it: its date and the instants it starts and ends in the person's time zone, which only the browser knows, 23 to 25 hours apart. It answers the meetings, all-day ones first and then by their start, without cancelled ones and ones the person declined, and the open tasks due on the date; a To Do due date written as the instant of midnight in the person's time zone counts for the day that instant falls in. A meeting whose location or description holds an address of one of the tenant's rooms, on one of the tenant's hosts, names the room. Connections that could not be read are listed apart, expired or active when the provider failed this time. An account reads its agenda ten times in a row, then once every 30 seconds, which spares the platform's quotas at the providers; the client reads it every five minutes.
Reminders
The client reminds the person five minutes before each meeting that is not all-day, also of a meeting it learns of when it is closer than that: with a browser notification while the page is in the background, and with a reminder on the page, with Go to the room where the meeting names one, until the person dismisses it. Neither comes while the person turned the event meeting_reminder off or does not want to be disturbed. Reminders come only while the app is open, as notifications do.
Development
A development platform offers the development calendar where VWS_ENCRYPTION_KEY is set, whether or not it has OAuth clients. It grants at once, without leaving the platform, as calendar@development.test, and each of its days holds an all-day "Office day", a "Stand-up" in the seeded room /dev/office that starts five minutes and twenty seconds after the connection, so that its reminder comes twenty seconds after connecting, a "Planning" meeting at two in the afternoon, and two tasks.
Calendar files
A meeting's calendar file is an iCalendar file (RFC 5545, METHOD:PUBLISH) with the meeting's title, start, end and status, and its address as LOCATION and URL: the room's for members, from GET /api/v1/meetings/{meeting}/calendar, and the meeting's link for guests, from GET /api/v1/invites/{token}/calendar and as the attachment meeting.ics of every invitation email. Its description says how to join, in the language of the request or of the email, with the outside tool's link where the meeting has one. The files of one meeting share its UID, so a calendar that imports two keeps one event. The page of a meeting's link shows the meeting from the link's creation, with the time the link opens, so that a guest can add it to their calendar before the meeting.
Building it
- Modules are built: the manifest's schema and checks, the administration of modules and their settings, permissions in the scripting host, the module places of the client, module areas in the map format and the editor, and the API key and webhook of a module's server side. While developing, a module may be served over http from the developer's machine, whose manifest a development platform fetches, whose pages it frames and whose icons it shows.
- Calendars and tasks are built: the OAuth connections for Google and Microsoft with their sealed tokens, the development calendar, the day's meetings and tasks with the rooms they name, reminders, and the calendar files of scheduled meetings, which invitation emails carry.
Found a mistake? Tell us.