UNPKG

@superawesome/permissions

Version:

Fine grained permissions / access control with ownerships & attribute picking, done right.

761 lines (621 loc) 30.1 kB
<!doctype html> <html class="no-js" lang=""> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <title>SuperAwesome Permissions (@superawesome/permissions)</title> <meta name="description" content=""> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="../images/favicon.ico"> <link rel="stylesheet" href="../styles/style.css"> <link rel="stylesheet" href="../styles/postmark.css"> </head> <body> <div class="navbar navbar-default navbar-fixed-top visible-xs"> <a href="../" class="navbar-brand">SuperAwesome Permissions (@superawesome/permissions)</a> <button type="button" class="btn btn-default btn-menu ion-ios-menu" id="btn-menu"></button> </div> <div class="xs-menu menu" id="mobile-menu"> <div id="book-search-input" role="search"><input type="text" placeholder="Type to search"></div> <compodoc-menu></compodoc-menu> </div> <div class="container-fluid main"> <div class="row main"> <div class="hidden-xs menu"> <compodoc-menu mode="normal"></compodoc-menu> </div> <!-- START CONTENT --> <div class="content interface"> <div class="content-data"> <ol class="breadcrumb"> <li>Interfaces</li> <li>IPermissionsOptions</li> </ol> <ul class="nav nav-tabs" role="tablist"> <li class="active"> <a href="#info" role="tab" id="info-tab" data-toggle="tab" data-link="info">Info</a> </li> <li > <a href="#source" role="tab" id="source-tab" data-toggle="tab" data-link="source">Source</a> </li> </ul> <div class="tab-content"> <div class="tab-pane fade active in" id="c-info"> <p class="comment"> <h3>File</h3> </p> <p class="comment"> <code>src/Permissions.ts</code> </p> <p class="comment"> <h3>Description</h3> </p> <p class="comment"> <p>The options passed at the <code>Permissions</code> constructor</p> </p> <section> <h3 id="index">Index</h3> <table class="table table-sm table-bordered index-table"> <tbody> <tr> <td class="col-md-4"> <h6><b>Properties</b></h6> </td> </tr> <tr> <td class="col-md-4"> <ul class="index-list"> <li> <span class="modifier">Optional</span> <a href="#limitOwnReduce">limitOwnReduce</a> </li> <li> <span class="modifier">Optional</span> <a href="#permissionDefinitionDefaults">permissionDefinitionDefaults</a> </li> <li> <span class="modifier">Optional</span> <a href="#permissionDefinitions">permissionDefinitions</a> </li> </ul> </td> </tr> </tbody> </table> </section> <section> <h3 id="inputs">Properties</h3> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <a name="limitOwnReduce"></a> <span class="name"><b>limitOwnReduce</b><a href="#limitOwnReduce"><span class="icon ion-ios-link"></span></a></span> </td> </tr> <tr> <td class="col-md-4"> <code>limitOwnReduce: <code><a href="../miscellaneous/typealiases.html#TlimitOwnReduce" target="_self" >TlimitOwnReduce&lt;TUserId | any&gt;</a></code> </code> </td> </tr> <tr> <td class="col-md-4"> <i>Type : </i> <code><a href="../miscellaneous/typealiases.html#TlimitOwnReduce" target="_self" >TlimitOwnReduce&lt;TUserId | any&gt;</a></code> </td> </tr> <tr> <td class="col-md-4"> <i>Optional</i> </td> </tr> </tbody> </table> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <a name="permissionDefinitionDefaults"></a> <span class="name"><b>permissionDefinitionDefaults</b><a href="#permissionDefinitionDefaults"><span class="icon ion-ios-link"></span></a></span> </td> </tr> <tr> <td class="col-md-4"> <code>permissionDefinitionDefaults: <code><a href="../classes/PermissionDefinitionDefaults.html" target="_self" >PermissionDefinitionDefaults</a></code> </code> </td> </tr> <tr> <td class="col-md-4"> <i>Type : </i> <code><a href="../classes/PermissionDefinitionDefaults.html" target="_self" >PermissionDefinitionDefaults</a></code> </td> </tr> <tr> <td class="col-md-4"> <i>Optional</i> </td> </tr> </tbody> </table> <table class="table table-sm table-bordered"> <tbody> <tr> <td class="col-md-4"> <a name="permissionDefinitions"></a> <span class="name"><b>permissionDefinitions</b><a href="#permissionDefinitions"><span class="icon ion-ios-link"></span></a></span> </td> </tr> <tr> <td class="col-md-4"> <code>permissionDefinitions: <code><a href="../miscellaneous/typealiases.html#PermissionDefinition" target="_self" >PermissionDefinition&lt;TUserId | TResourceId&gt; | PermissionDefinition&lt;TUserId, TResourceId&gt;[]</a></code> </code> </td> </tr> <tr> <td class="col-md-4"> <i>Type : </i> <code><a href="../miscellaneous/typealiases.html#PermissionDefinition" target="_self" >PermissionDefinition&lt;TUserId | TResourceId&gt; | PermissionDefinition&lt;TUserId, TResourceId&gt;[]</a></code> </td> </tr> <tr> <td class="col-md-4"> <i>Optional</i> </td> </tr> </tbody> </table> </section> </div> <div class="tab-pane fade tab-source-code" id="c-source"> <pre class="line-numbers compodoc-sourcecode"><code class="language-typescript">import * as _ from &#x27;lodash&#x27;; import * as _f from &#x27;lodash/fp&#x27;; import { diff } from &#x27;json-diff&#x27;; import { AccessControl, IQueryInfo, Permission } from &#x27;accesscontrol&#x27;; // own import { AccessControlRe } from &#x27;accesscontrol-re&#x27;; import { EPossession, GrantPermitQuery, isValidIUser, Tid, TisOwner, TlimitOwned, TlimitOwnReduce, TlistOwned, } from &#x27;./types&#x27;; import { buildAccessControl, deleteEmptyArrayKeys, hasSomeOwnGrant, stringify, projectPDWithDefaultsToInternal, } from &#x27;./utils&#x27;; import { consolidatePermissionDefinitions } from &#x27;./consolidations&#x27;; import { Permit } from &#x27;./Permit.class&#x27;; import { PermissionDefinition, PermissionDefinitionDefaults, PermissionDefinitionInternal, } from &#x27;./PermissionDefinitions&#x27;; import { getLogger } from &#x27;./logger&#x27;; /** The options passed at the &#x60;Permissions&#x60; constructor */ export interface IPermissionsOptions&lt; TUserId extends Tid &#x3D; number, TResourceId extends Tid &#x3D; number &gt; { permissionDefinitions?: | PermissionDefinition&lt;TUserId, TResourceId&gt; | PermissionDefinition&lt;TUserId, TResourceId&gt;[]; permissionDefinitionDefaults?: PermissionDefinitionDefaults; limitOwnReduce?: TlimitOwnReduce&lt;TUserId, any&gt;; } /** The main class - see [Basic Usage](/additional-documentation/basic-usage.html) */ export class Permissions&lt;TUserId extends Tid &#x3D; number, TResourceId extends Tid &#x3D; number&gt; { private _permissionDefinitionsInternal: PermissionDefinitionInternal[] &#x3D; []; private _accessControl: AccessControl; private _acre: AccessControlRe; private _rolesNotFound &#x3D; {}; private roles: string[]; private _limitOwnReduce: TlimitOwnReduce&lt;TUserId, any&gt;; private _isBuilt &#x3D; false; constructor({ permissionDefinitions, permissionDefinitionDefaults, limitOwnReduce, }: IPermissionsOptions&lt;TUserId, TResourceId&gt; &#x3D; {}) { this._limitOwnReduce &#x3D; limitOwnReduce; this.addDefinitions(permissionDefinitions || [], permissionDefinitionDefaults); } public addDefinitions( permissionDefinitions: | PermissionDefinition&lt;TUserId, TResourceId&gt; | PermissionDefinition&lt;TUserId, TResourceId&gt;[], permissionDefinitionDefaults: PermissionDefinitionDefaults &#x3D; {} ) { this.ensureHasNotBuild(); if (!permissionDefinitions) throw new Error( &#x60;SA-Permissions: in addDefinitions(), invalid permissionDefinitions: ${stringify( permissionDefinitions )}&#x60; ); if (!_.isArray(permissionDefinitions)) permissionDefinitions &#x3D; [permissionDefinitions]; const ipdsToAdd &#x3D; permissionDefinitions.map( projectPDWithDefaultsToInternal(permissionDefinitionDefaults) ); // sanity checks before adding ipds _.each(ipdsToAdd, (ipdToAdd, ipdToAddIdx): any &#x3D;&gt; { // if we are trying to redefine a role+resource+action:possession // with DIFFERENT attributes (i.e non-strict) throw as its very dangerous! const nonStrictDuplicatePds &#x3D; this.filterPDsWithDuplicateGrantActions(ipdToAdd); if (!_.isEmpty(nonStrictDuplicatePds)) { const firstConflictingAction &#x3D; _.findKey( ipdToAdd.grant, (attributes, action) &#x3D;&gt; !!nonStrictDuplicatePds[0].grant[action] ); throw new Error( &#x60;SA-Permissions: InvalidPermissionDefinitionError: addDefinitions() redefining action error. Action: &quot;${firstConflictingAction}&quot; Action Attributes: ${stringify(ipdToAdd.grant[firstConflictingAction])} While adding PD: ${stringify(ipdToAdd)} Conflicted with PD: ${stringify(nonStrictDuplicatePds[0])}&#x60; ); } // if we are trying to redefine a role+resource+action:possession // even with SAME different attributes (i.e very strict) warn as obsolete! const strictDuplicatePds &#x3D; this.filterPDsWithDuplicateGrantActions(ipdToAdd, true); if (!_.isEmpty(strictDuplicatePds)) { const firstConflictingAction &#x3D; _.findKey( ipdToAdd.grant, (attributes, action) &#x3D;&gt; !!strictDuplicatePds[0].grant[action] ); getLogger().warn( &#x60;addDefinitions() redefining action in a PD with same attributes is obsolete:&#x60;, { action: firstConflictingAction, attributes: ipdToAdd.grant[firstConflictingAction], permissionDefinition: ipdToAdd, } ); } if (hasSomeOwnGrant(ipdToAdd)) { const isOwnerFound &#x3D; !!ipdToAdd.isOwner; let listOwnedFound &#x3D; !!ipdToAdd.listOwned; let limitOwnedFound &#x3D; !!ipdToAdd.limitOwned; // on this PD if (listOwnedFound &amp;&amp; limitOwnedFound) throw new Error( &#x60;SA-Permissions: in addDefinitions() found BOTH &quot;listOwned&quot; &amp; &quot;limitOwned&quot; callbacks in the added PermissionDefinition. Use one or the other, but not both. PermissionDefinition &#x3D; ${JSON.stringify( permissionDefinitions[ipdToAddIdx], null, 2 )}&#x60; ); // It has some OWN Grant, but no owner hooks found, throw if (!isOwnerFound || (!listOwnedFound &amp;&amp; !limitOwnedFound)) { throw new Error( &#x60;SA-Permissions: in addDefinitions() PermissionDefinition has &#x27;own&#x27; action but no ${ !isOwnerFound ? &#x27;&quot;isOwner&quot;&#x27; : &#x27;&quot;listOwned&quot; nor &quot;limitOwned&quot;&#x27; } callbacks are there. PermissionDefinition &#x3D; ${stringify(ipdToAdd)} &#x60; ); } // check all for same resource as ipdToAdd let conflictedPD; for (const opd of this._permissionDefinitionsInternal) { if (ipdToAdd.resource &#x3D;&#x3D;&#x3D; opd.resource) { listOwnedFound &#x3D; listOwnedFound || !!opd.listOwned; limitOwnedFound &#x3D; limitOwnedFound || !!opd.limitOwned; } if (listOwnedFound &amp;&amp; limitOwnedFound) { conflictedPD &#x3D; opd; break; } } if (listOwnedFound &amp;&amp; limitOwnedFound) throw new Error( &#x60;SA-Permissions: in addDefinitions() found BOTH &quot;listOwned&quot; &amp; &quot;limitOwned&quot; callbacks in some PermissionDefinition for resource &quot;${ ipdToAdd.resource }&quot;. Use one or the other, but not both. Adding PD: ${stringify(permissionDefinitions[ipdToAddIdx])} Conflicted with PD: ${stringify(conflictedPD)}&#x60; ); } this._permissionDefinitionsInternal.push(ipdToAdd); // all ok, add it! }); } /** * Check is this Permissions instance has been built (so no more .addDefinitions() allowed) */ public get isBuilt(): boolean { return this._isBuilt; } public build() { this._isBuilt &#x3D; true; if (this._acre) return this; [this._accessControl, this._acre] &#x3D; buildAccessControl(this._permissionDefinitionsInternal); this.roles &#x3D; this.getRoles(); return this; } /** The &#x60;grantPermit()&#x60; is the way to *query* the Permissions instance for granting permissions to a User. The method responds with an instance of [Permit](/classes/Permit.html) that holds all known information about the queried **user**, **resource** and **action**. In short, the question is &quot;can some of &#x60;user.roles&#x60; perform &#x60;action&#x60; either a) on **any** &#x60;resource&#x60; or b) on an **own** &#x60;resource&#x60; (AND the specific &#x60;resourceId&#x60; if passed)? We are checking all roles for both **any** &amp; **own**, while collecting all &#x60;isOwner&#x60; &amp; &#x60;listOwned&#x60; and feed all known information into a **Permit** object. @return Promise&lt;Permit&gt; a Promise of a [Permit](/classes/Permit.html) instance. */ public async grantPermit({ // &lt;TUserId extends Tid &#x3D; number, TResourceId extends Tid &#x3D; number&gt; user, action, resource, resourceId, }: GrantPermitQuery&lt;TUserId, TResourceId&gt;): Promise&lt;Permit&lt;TUserId, TResourceId&gt;&gt; { this.ensureHasBuild(); if (!isValidIUser(user)) throw new Error( &#x27;SA-Permissions: at grantPermit(), user is not a valid &#x60;interface IUser {id: TId; roles: string[];}&#x60;&#x27; ); if (!this.getResources().includes(resource)) throw new Error(&#x60;SA-Permissions: at grantPermit(), Invalid resource: &quot;${resource}&quot;&#x60;); if (action.split(&#x27;:&#x27;).length &gt; 1) throw new Error( &#x60;SA-Permissions: at grantPermit(), Invalid action structure: &quot;${action}&quot;. The colon &quot;:&quot; in the action is not allowed on grantPermit() and you must NOT specify &quot;:own&quot; or &quot;:any&quot; after the action at it. SA-Permissions always returns a Permit that checks for both any &amp; own.&#x60; ); let acPermission: Permission; // &#x3D; { granted: false } as any; let anyAcPermission: Permission; let ownAcPermission: Permission; // The &#x60;Permit&#x60; values const isOwners: TisOwner&lt;TUserId, TResourceId&gt;[] &#x3D; []; const listOwneds: TlistOwned&lt;TUserId, TResourceId&gt;[] &#x3D; []; const limitOwneds: TlimitOwned&lt;any, TUserId&gt;[] &#x3D; []; // 2 passes: check all EPossession against all roles. // if any permissions.granted is true, granted is true // but continue to gather all permissions.attributes, isOwner &amp; listOwned for (const queryPossession of [EPossession.any, EPossession.own]) { getLogger().debug(&#x27;grantPermit: possession&#x27;, { possession: queryPossession }); const unknownRoles &#x3D; _.difference(user.roles, this.roles); const roles &#x3D; _.without(user.roles, ...unknownRoles); _.each(unknownRoles, (rl) &#x3D;&gt; { if (!this._rolesNotFound[rl]) { this._rolesNotFound[rl] &#x3D; true; getLogger().warn( &#x60;SA-Permissions(): at grantPermit(), role not found: ${rl} (will not warn again about this role)&#x60; ); } }); const queryInfo: IQueryInfo &#x3D; { role: roles, action: &#x60;${action}:${queryPossession}&#x60;, resource, }; try { acPermission &#x3D; this._acre.permission(queryInfo); } catch (error) { // @todo: handle throw error; } getLogger().debug(&#x27;grantPermit: this._accessControl.permission(queryInfo)&#x27;, { queryInfo, &#x27;permission.granted&#x27;: acPermission.granted, &#x27;permission.attributes&#x27;: acPermission.attributes, }); switch (queryPossession) { case EPossession.any: { anyAcPermission &#x3D; acPermission; break; } case EPossession.own: { ownAcPermission &#x3D; acPermission; if (ownAcPermission.granted) { const matchingCpds &#x3D; _.filter(this._permissionDefinitionsInternal, (pd) &#x3D;&gt; { return ( _.some(pd.roles, (pdRole) &#x3D;&gt; user.roles.includes(pdRole)) &amp;&amp; (resource &#x3D;&#x3D;&#x3D; pd.resource || pd.resource &#x3D;&#x3D;&#x3D; &#x27;*&#x27;) &amp;&amp; (!!(pd?.grant || {})[&#x60;${action}:${EPossession.own}&#x60;] || !!(pd?.grant || {})[&#x60;*:${EPossession.own}&#x60;] || !!(pd?.grant || {})[&#x60;${action}:${EPossession.any}&#x60;] || !!(pd?.grant || {})[&#x60;*:${EPossession.any}&#x60;]) ); }); // prettier-ignore if (!anyAcPermission.granted &amp;&amp; _.isEmpty(matchingCpds)) throw new Error( &#x60;SA-Permissions: own access granted but no matching PermissionDefinitions found: &#x60; + &#x60;${stringify({ user, action, resource })}&#x60;, ); _.each(matchingCpds, (cpd) &#x3D;&gt; { const { isOwner, listOwned, limitOwned } &#x3D; cpd; if (isOwner) isOwners.push(isOwner as any); if (listOwned) listOwneds.push(listOwned as any); if (limitOwned) limitOwneds.push(limitOwned as any); }); } break; } default: throw new Error( &#x60;SA-Permissions::grantPermit: invalid EPossession in queryPossession &quot;${queryPossession}&quot;&#x60; ); } } const permit &#x3D; new (Permit as any)( // constructor is best kept private, only we should use it! user, action, resource, resourceId, anyAcPermission, ownAcPermission, _.uniq(isOwners), _.uniq(listOwneds), _.uniq(limitOwneds), this._limitOwnReduce ); // prettier-ignore if (!anyAcPermission.granted &amp;&amp; ownAcPermission.granted) { // The following checks SHOULD NOT be needed, they should be caught at the addDefinitions() call. Please report to authors if you encounter them. const createError &#x3D; (butDetail: string) &#x3D;&gt; new Error(&#x60;SA-Permissions: grantPermit() &quot;OWN&quot; access granted but ${butDetail }. The error should have been caught at addDefinitions() call, please report to authors. GrantPermitQuery &#x3D; ${ stringify({ user, action, resource, resourceId })}&#x60;); if (_.isEmpty(isOwners)) throw createError(&#x27;no &quot;isOwner&quot; ownership hook found&#x27;); if (_.isEmpty(listOwneds) &amp;&amp; _.isEmpty(limitOwneds)) throw createError(&#x27;no &quot;listOwned&quot; nor &quot;limitOwned&quot; ownership hooks found&#x27;); if (!_.isEmpty(listOwneds) &amp;&amp; !_.isEmpty(limitOwneds)) throw createError(&#x27;found BOTH &quot;listOwned&quot; &amp; &quot;limitOwned&quot; ownership hooks. Use one or the other, but not both&#x27;); if (resourceId) (permit as any).resourceIdOwnPermissionGranted &#x3D; await permit.isOwn(resourceId); } return permit as Permit&lt;TUserId, TResourceId&gt;; } // some helpers public getRoles(): string[] { this.ensureHasBuild(); return this._acre.getRoles(); } public getResources(): string[] { this.ensureHasBuild(); return this._acre.getResources(); } public getActions(): string[] { this.ensureHasBuild(); return this._acre.getActions(); } /** * Returns a deep clone of [&#x60;AccessControl#getGrants()&#x60;](https://onury.io/accesscontrol/?api&#x3D;ac#AccessControl#getGrants) (which according to its docs &#x60;Gets the internal grants object that stores all current grants.&#x60;), but omitting empty arrays eg &#x60;&#x27;rollover:any&#x27;: []&#x60;. * * @see https://onury.io/accesscontrol/?api&#x3D;ac#AccessControl#getGrants */ public getGrants(): object { // @todo: typings this.ensureHasBuild(); // delete empty arrays, eg &#x60;&#x27;rollover:any&#x27;: []&#x60; cause they are useless &amp; break our &#x60;compare()&#x60; return deleteEmptyArrayKeys(_.cloneDeep(this._accessControl.getGrants())); } public compare(permissions1: Permissions&lt;any, any&gt;, permissions2: Permissions&lt;any, any&gt; &#x3D; this) { return diff(permissions1.getGrants(), permissions2.getGrants()); } // Grab accessControl.getGrants(), BUT delete all empty / denied grants that /** Returns a list of the &#x60;PermissionDefinition&#x60; objects stored in this instance, with optional filtering &amp; consolidations removing duplicates and redundant grants (**WARNING**: this is experimental) @param filter allows you to filter PDs: * Use an object eg &#x60;{ resource: &#x27;document&#x27; }&#x60; as the &#x60;_.matches&#x60; iteratee shorthand. If this &#x60;_.matches&#x60; object is used, the props used for filtering are considered &quot;default&quot; and are omitted from each PD. * OR use a function returning boolean for each PD, eg (pd) &#x3D;&gt; pd.resource &#x3D;&#x3D;&#x3D; &#x27;document&#x27; See https://lodash.com/docs/4.17.11#filter @param consolidateFlag is **experimental**, it tries to consolidate PermissionDefinitions, remove duplicates and merge compatible ones */ public getDefinitions( filter?: { [key: string]: any; }, consolidateFlag: boolean | &#x27;force&#x27; &#x3D; false ): Partial&lt;PermissionDefinitionInternal&gt;[] { const filteredPDs &#x3D; _f.flow( _f.filter(filter), _f.reject((opd) &#x3D;&gt; _.isEmpty(opd.grant)) )(this._permissionDefinitionsInternal); const resultPDs &#x3D; consolidateFlag ? consolidatePermissionDefinitions( filter, consolidateFlag )(_.cloneDeep(this._permissionDefinitionsInternal)) : filteredPDs; const resultedSaPermissions &#x3D; new Permissions({ permissionDefinitions: resultPDs, permissionDefinitionDefaults: filter, }).build(); const filteredInstancePermissions &#x3D; new Permissions({ permissionDefinitions: filteredPDs as any, permissionDefinitionDefaults: filter, }).build(); const difference &#x3D; this.compare(resultedSaPermissions, filteredInstancePermissions); if (difference !&#x3D;&#x3D; undefined) { throw new Error( &#x60;SA-Permissions: getDefinitions diff: ${stringify(difference)} Existing grants: ${stringify(this.getGrants())} Generated grants: ${stringify(resultedSaPermissions.getGrants())} &#x60; ); } return resultPDs; } private ensureHasBuild() { if (!this._acre) throw new Error( &#x60;SA-Permissions InvalidInvocation: calling permissions methods before having build()&#x60; ); } private ensureHasNotBuild() { if (this._acre) throw new Error( &#x60;SA-Permissions InvalidInvocation: calling addDefinitions() after having build()&#x60; ); } /** * * @param pdi a PermissionDefinitionInternal * @param strict true means we dont care if redefining action is _.equal. Duplicating is bad enough! */ private filterPDsWithDuplicateGrantActions &#x3D; ( pdi: PermissionDefinitionInternal, strict &#x3D; false ) &#x3D;&gt; _.filter( this._permissionDefinitionsInternal, (originalIpd) &#x3D;&gt; _.isEqual(originalIpd.resource, pdi.resource) &amp;&amp; _.some(pdi.roles, (ipdToAddRole) &#x3D;&gt; _.includes(originalIpd.roles, ipdToAddRole)) &amp;&amp; _.some( pdi.grant, (attributes, action) &#x3D;&gt; !!originalIpd.grant[action] &amp;&amp; (strict || !_.isEqual(originalIpd.grant[action], pdi.grant[action])) ) ); } </code></pre> </div> </div> </div><div class="search-results"> <div class="has-results"> <h1 class="search-results-title"><span class='search-results-count'></span> result-matching "<span class='search-query'></span>"</h1> <ul class="search-results-list"></ul> </div> <div class="no-results"> <h1 class="search-results-title">No results matching "<span class='search-query'></span>"</h1> </div> </div> </div> <!-- END CONTENT --> </div> </div> <script> var COMPODOC_CURRENT_PAGE_DEPTH = 1; var COMPODOC_CURRENT_PAGE_CONTEXT = 'interface'; var COMPODOC_CURRENT_PAGE_URL = 'future-roadmap.html'; </script> <script src="../js/libs/custom-elements.min.js"></script> <script src="../js/libs/lit-html.js"></script> <!-- Required to polyfill modern browsers as code is ES5 for IE... --> <script src="../js/libs/custom-elements-es5-adapter.js" charset="utf-8" defer></script> <script src="../js/menu-wc.js" defer></script> <script src="../js/libs/bootstrap-native.js"></script> <script src="../js/libs/es6-shim.min.js"></script> <script src="../js/libs/EventDispatcher.js"></script> <script src="../js/libs/promise.min.js"></script> <script src="../js/libs/zepto.min.js"></script> <script src="../js/compodoc.js"></script> <script src="../js/tabs.js"></script> <script src="../js/menu.js"></script> <script src="../js/libs/clipboard.min.js"></script> <script src="../js/libs/prism.js"></script> <script src="../js/sourceCode.js"></script> <script src="../js/search/search.js"></script> <script src="../js/search/lunr.min.js"></script> <script src="../js/search/search-lunr.js"></script> <script src="../js/search/search_index.js"></script> <script src="../js/lazy-load-graphs.js"></script> </body> </html>