diff --git a/design/images/permission.dialog.html b/design/images/permission.dialog.html
new file mode 100644
index 000000000..ad04e28c0
--- /dev/null
+++ b/design/images/permission.dialog.html
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
"Wear A Hat" (🔓 mres.altvr.com) requests the following permissions:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<
+ • • •
+
>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🔓 mres.altvr.com
+ Wear A Hat, Functional Tests
+
+
Execution, User Interaction, User Tracking
+
+
🖉
+
🗙
+
+
+
+
+ 🔓 secure-mre.azurewebsites.net
+ Where In The World
+
+
Execution, User Interaction
+
+
🖉
+
🗙
+
+
+
+
+ 🔓 helmets-mre.azurewebsites.net
+ Helmets
+
+
Execution, User Interaction
+
+
🖉
+
🗙
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/design/images/permission.overview.png b/design/images/permission.overview.png
new file mode 100644
index 000000000..872a185eb
Binary files /dev/null and b/design/images/permission.overview.png differ
diff --git a/design/images/permission.prompt.png b/design/images/permission.prompt.png
new file mode 100644
index 000000000..bc4e07684
Binary files /dev/null and b/design/images/permission.prompt.png differ
diff --git a/design/images/permission.prompt.xcf b/design/images/permission.prompt.xcf
new file mode 100644
index 000000000..565c6d020
Binary files /dev/null and b/design/images/permission.prompt.xcf differ
diff --git a/design/images/permission.requests.png b/design/images/permission.requests.png
new file mode 100644
index 000000000..58d001632
Binary files /dev/null and b/design/images/permission.requests.png differ
diff --git a/design/images/permission.settings.png b/design/images/permission.settings.png
new file mode 100644
index 000000000..bbd3b4656
Binary files /dev/null and b/design/images/permission.settings.png differ
diff --git a/design/permissions.md b/design/permissions.md
new file mode 100644
index 000000000..27e52bd96
--- /dev/null
+++ b/design/permissions.md
@@ -0,0 +1,195 @@
+Permissions API
+=================
+
+To be a responsible member of the Social MR ecosystem, the MRE SDK has to prioritize the safety of its residents over
+the power of this SDK. As such, I propose the addition of a permissions API, so MREs can have more "invasive" features
+without compromising user safety and agency.
+
+
+Permission scope
+------------------
+
+When a user approves or denies permission to an MRE, the scope of that decision is determined by the host app,
+and is based on the MRE URL. To mimic how Chromium manages permissions, hosts could associate decisions with
+unique combinations of URL scheme, host, and port, known collectively as the app "origin". For a more narrow
+permissioning system, the URL path could also be included, but it is not recommended that the other parts of a URL
+be considered: username, password, anchor, or query arguments.
+
+
+Features requiring user permission
+------------------------------------
+
+If a permission is not requested, it is considered denied.
+
+* `execution` (client only) - Required to connect to an MRE server. Typically granted by default, but can be revoked.
+ * Allow: MRE connection can be established
+ * Deny: MRE connection will not be established
+* `user-tracking` - Grants access to a persistent user identity across sessions. Needed for things like high scores lists.
+ * Allow: This user will be uniquely identified to this MRE origin across sessions and instances.
+ * Deny: This user will be assigned a new ID every time they connect to MREs from this origin. If the `user-interaction`
+ permission is also denied, this client will not join a user to the session at all.
+* `user-interaction` - Behaviors, exclusive actors, attachments, and dialogs.
+ * Allow: This user can interact with behaviors, exclusively own actors, be a target for attached actors, and can be
+ sent dialogs.
+ * Deny: Interactions with behaviors will not be sent back to the app server. Attempts to create exclusive actors
+ for this user will fail. Actors attached to this user will be considered unattached. Calls to `user.prompt`
+ will be rejected. If the `user-tracking` permission is also denied, this client will not join a user to the
+ session at all.
+* Play sounds and video
+* Large assets (hypothetical) - If an MRE wants to load more than some large amount of assets into memory
+ (30MB worth of memory? TBD), the client must first approve. This permission might automatically be approved/denied
+ by clients on behalf of users based on device capabilities.
+* Movement (hypothetical) - The ability to forcibly move a user's avatar and point of view, either smoothly
+ or teleported.
+* Microphone input (hypothetical) - The ability for users to stream their microphone input into an MRE, for voice
+ commands, synthesizers, chat bots, or anything else.
+
+
+Permission declaration
+------------------------
+
+Apps must declare which APIs they will use in advance of any users connecting. This is done via a JSON-formatted
+manifest loaded from the app's HTTP root. If the MRE is served over a secure websocket (`wss://`), the manifest should
+be loaded using HTTPS instead. For example, the manifest for the URL `ws://mres.altvr.com/tests/red` should
+be found at `http://mres.altvr.com/tests/red/manifest.json`. Note that this file can be served from the filesystem or
+constructed on request.
+
+The manifest can contain lots of different fields in addition to permissions, but must conform to the included
+[JSON schema](../packages/sdk/src/util/manifestSchema.json).
+
+
+Permissioning flow
+--------------------
+
+Legend:
+
+* [App] Step executed by MRE developer code
+* [SDK] Step executed by MRE SDK
+* [Runtime] Step executed in client-side MRE runtime
+* [Host] Step executed by host application
+
+Startup:
+
+1. [App] During development, the app developer authors a static manifest file, or uses the WebHost API to generate one.
+2. [Runtime|Host] Initializes the MRE API with a permissions manager instance that will receive new permission requests.
+ If permission decisions are persistent, load old decisions into memory now.
+3. [Host] The host wishes to run an MRE, so creates an IMixedRealityExtensionApp instance and calls Startup().
+4. [Runtime] Downloads the manifest from the provided MRE server. If missing, assumes no permissions but `execution`
+ are required.
+5. [Runtime] Calls into the permission manager requesting the manifest-listed permissions.
+6. [Host] Permission manager evaluates the required and optional MRE permissions against the current set of decisions
+ for the MRE's URL, and determines if any new permission decisions need to be made by the user. If so:
+ 1. [Host] Present the choices to the user by whatever means the host sees fit, persist the decision if desired,
+ and report the result to the Runtime. Optional permissions should be presented in such a way as they can be
+ granted or denied individually, but required permissions should be decided as a group.
+7. [Runtime] Determine if the MRE has sufficient permission to run, i.e. `execution` and all the required permissions
+ are granted. If not, abort connecting to the MRE.
+8. [Runtime] Startup proceeds like normal, but if a `user-joined` message is sent, the user payload must include
+ the IDs of any permissions that were granted to this MRE by this user (other than `execution`, which is implied).
+
+
+Permissions Manager
+---------------------
+
+Host apps must provide hooks for the MRE subsystem to obtain permission from users. This is provided to
+`MREAPI.InitializeAPI` as an implementation of `IPermissionManager`, which has the following methods:
+
+* `Task PromptForPermission(...)` - Request permissions from the user, and return a Task that
+ resolves with those permissions the user has granted. Takes the following arguments:
+ * `Uri appLocation` - The URI of the MRE requesting permission, as described above.
+ * `IEnumerable permissionsNeeded` - A list of the permissions required to run the app.
+ * `IEnumerable permissionsWanted` - A list of permissions that the app can use, but are not
+ needed to run.
+ * `Permissions permissionFlagsNeeded` - Same as `permissionsNeeded`, but in a bitfield.
+ * `Permissions permissionFlagsWanted` - Same as `permissionsWanted`, but in a bitfield.
+* `Permissions CurrentPermissions(...)` - Returns a bitfield of the currently granted permissions. Takes
+ the following arguments:
+ * `Uri appLocation` - The URI of the MRE for which we want the permission set.
+* `event Action OnPermissionDecisionsChanged`
+
+The `Permissions` enum has the `Flags` attribute and is on powers of 2 in case testing combinations is desired.
+
+```cs
+[Flags]
+enum Permissions {
+ None = 0,
+ Execution = 1,
+ UserTracking = 2,
+ UserInteraction = 4,
+ ...
+}
+```
+
+
+UI Mockups
+============
+
+This is what an MRE permissions management interface could look like. When an MRE is available to run in the current
+host app context, a button to bring up the management UI should be readily available (within two clicks of steady-state
+usage). Attention should be drawn to this button when a new MRE permission request comes in.
+
+Clicking the prompt or the button brings up your MRE permission requests:
+
+![Requests](images/permission.requests.png)
+
+This window is paginated, one page per MRE that requires permissions. The name of the app requesting permission, the
+server name it's hosted on, and a secure protocol indicator are all featured prominently. Each permission has a
+checkbox next to it: required permissions are checked and disabled, and optional permissions are checked or not based
+on app origin decision history and settings.
+
+A global MRE settings menu could also be available:
+
+![Settings](images/permission.settings.png)
+
+From this UI, the user could choose to grant a certain permission to all MREs globally.
+
+There could also be UI to retroactively change your permission decisions:
+
+![Overview](images/permission.overview.png)
+
+This panel displays an overview of each MRE available in the current host app context, what permissions the user has
+granted, and some controls. One button lets the user modify their permission decisions by re-opening the Requests view
+for that app, and if that app is currently running, reconnect. The other button deletes the saved decisions for that
+app origin.
+
+The "Here" and "All" tabs in this mockup are laid out the same, except "Here" is only the MREs in the current context,
+whereas "All" is the list of all MRE origins for which there are stored decisions.
+
+
+Old stuff
+===========
+
+
+Methods for acquiring permission
+----------------------------------
+
+1. **Request permission for a feature the first time it is used**
+ 1. The application code makes an async call to an API that requires user permission.
+ 2. The SDK detects that this is the first time that API is used for this user, so it saves the API call request
+ internally and sends a permission request to the user.
+ 3. The user's client will reply with an approved or denied message. If approved, the original API call is executed.
+ If not, handle the rejection.
+2. **Request permission for a set of features explicitly**
+ 1. The application code uses a permissions API to set the permission requirements for the app.
+ 2. All current and late-joining users are sent a message asking for any permissions not already granted or denied.
+ 3. Each user will reply with an approved or denied message. If approved, the approval is saved, and all future API
+ requests that require that permission will be processed for this user. If denied, handle the rejection.
+3. **Establish permissions during initialization**
+ 1. Provide a list of permissions required by an app during app setup.
+ 2. During connection handshake, the list of required permissions will be sent to the client.
+ 3. If the client approves, initialization proceeds like normal. If not, handle the rejection.
+
+
+Methods for handling permission rejection
+-------------------------------------------
+
+1. Revoke access to the denied APIs for the set of denying users.
+2. All users must approve of all permissions; users that do not approve immediately leave the MRE.
+3. All clients must approve of all permissions; clients that do not approve immediately disconnect from the MRE server.
+
+
+Methods for presenting permission requests to users
+-----------------------------------------------------
+
+1. Dialogs
+2. Settings menu
diff --git a/packages/functional-tests/public/manifest.json b/packages/functional-tests/public/manifest.json
new file mode 100644
index 000000000..99003ad72
--- /dev/null
+++ b/packages/functional-tests/public/manifest.json
@@ -0,0 +1,8 @@
+{
+ "name": "MRE Functional Tests",
+ "description": "A set of samples that exercise various features of the Mixed Reality Extensions SDK",
+ "author": "Microsoft",
+ "license": "MIT",
+ "repositoryUrl": "https://github.com/microsoft/mixed-reality-extension-sdk",
+ "optionalPermissions": ["user-interaction"]
+}
diff --git a/packages/functional-tests/src/server.ts b/packages/functional-tests/src/server.ts
index 6efa0857f..92b451f0e 100644
--- a/packages/functional-tests/src/server.ts
+++ b/packages/functional-tests/src/server.ts
@@ -20,7 +20,7 @@ process.on('unhandledRejection', (reason) => console.log('unhandledRejection', r
// Start listening for connections, and serve static files
const server = new MRE.WebHost({
// baseUrl: 'http://.ngrok.io',
- baseDir: resolvePath(__dirname, '../public'),
+ baseDir: resolvePath(__dirname, '../public')
});
// Handle new application sessions
diff --git a/packages/sdk/package-lock.json b/packages/sdk/package-lock.json
index 0dc16f521..a1d71fabf 100644
--- a/packages/sdk/package-lock.json
+++ b/packages/sdk/package-lock.json
@@ -1164,6 +1164,11 @@
"graceful-fs": "^4.1.6"
}
},
+ "jsonschema": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.6.tgz",
+ "integrity": "sha512-SqhURKZG07JyKKeo/ir24QnS4/BV7a6gQy93bUSe4lUdNp0QNpIz2c9elWJQ9dpc5cQYY6cvCzgRwy0MQCLyqA=="
+ },
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index b37ba8e99..edc94f76e 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -54,6 +54,7 @@
"deepmerge": "^2.1.1",
"etag": "^1.8.1",
"forwarded-for": "^1.1.0",
+ "jsonschema": "^1.2.6",
"query-string": "^6.2.0",
"restify": "^8.5.1",
"restify-errors": "^8.0.2",
diff --git a/packages/sdk/src/core/contextInternal.ts b/packages/sdk/src/core/contextInternal.ts
index 525b15dcf..c1b2bfe0e 100644
--- a/packages/sdk/src/core/contextInternal.ts
+++ b/packages/sdk/src/core/contextInternal.ts
@@ -22,6 +22,7 @@ import {
MediaInstance,
newGuid,
PerformanceStats,
+ Permissions,
SetMediaStateOptions,
TriggerEvent,
User,
@@ -111,6 +112,27 @@ export class ContextInternal {
// Get a reference to the new actor.
const actor = this.context.actor(payload.actor.id);
+ // check permission for exclusive actors
+ let user: User;
+ if (actor.exclusiveToUser &&
+ (user = this.userSet.get(actor.exclusiveToUser)) &&
+ !(user.grantedPermissions.includes(Permissions.UserInteraction))) {
+ actor.internal.notifyCreated(false,
+ `Permission denied on user ${user.id} (${user.name}). Either this MRE did not ` +
+ "request the UserInteraction permission, or it was denied by the user."
+ );
+ }
+
+ // check permission for attachments
+ if (actor.attachment?.userId &&
+ (user = this.userSet.get(actor.attachment?.userId)) &&
+ !(user.grantedPermissions.includes(Permissions.UserInteraction))) {
+ actor.internal.notifyCreated(false,
+ `Permission denied on user ${user.id} (${user.name}). Either this MRE did not ` +
+ "request the UserInteraction permission, or it was denied by the user."
+ );
+ }
+
this.protocol.sendPayload( payload, {
resolve: (replyPayload: Payloads.ObjectSpawned | Payloads.OperationResult) => {
this.protocol.recvPayload(replyPayload);
diff --git a/packages/sdk/src/user/index.ts b/packages/sdk/src/user/index.ts
index 84e1e5c1a..a28159db2 100644
--- a/packages/sdk/src/user/index.ts
+++ b/packages/sdk/src/user/index.ts
@@ -6,3 +6,4 @@
export * from './groupMask';
export * from './invertedGroupMask';
export * from './user';
+export * from './permissions';
diff --git a/packages/sdk/src/user/permissions.ts b/packages/sdk/src/user/permissions.ts
new file mode 100644
index 000000000..c265429ef
--- /dev/null
+++ b/packages/sdk/src/user/permissions.ts
@@ -0,0 +1,25 @@
+/*!
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+/**
+ * The set of permissions MREs can request from users, and users can grant.
+ */
+export enum Permissions {
+ /**
+ * Grants access to a persistent user identity across sessions. Needed for things like high scores lists or
+ * persistent settings. Allow: This user will be uniquely identified to this MRE origin across sessions and
+ * instances. Deny: This user will be assigned a new ID every time they connect to MREs from this origin.
+ * If the `user-interaction` permission is also denied, this client will not join a user to the session at all.
+ */
+ UserTracking = 'user-tracking',
+ /**
+ * Allow: This user can interact with behaviors, exclusively own actors, be a target for attached actors, and can be
+ * sent dialogs. Deny: Interactions with behaviors will not be sent back to the app server. Attempts to create
+ * exclusive actors for this user will fail. Actors attached to this user will be considered unattached. Calls
+ * to `user.prompt` will be rejected. If the `user-tracking` permission is also denied, this client will not join
+ * a user to the session at all.
+ */
+ UserInteraction = 'user-interaction',
+}
diff --git a/packages/sdk/src/user/user.ts b/packages/sdk/src/user/user.ts
index 1293eae85..0f15cb83f 100644
--- a/packages/sdk/src/user/user.ts
+++ b/packages/sdk/src/user/user.ts
@@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/
-import { Context, GroupMask, Guid } from '..';
+import { Context, GroupMask, Guid, Permissions } from '..';
import { Patchable, readPath, } from '../internal';
import { UserInternal } from './userInternal';
@@ -11,6 +11,11 @@ export interface UserLike {
id: Guid;
name: string;
groups: number | GroupMask;
+ /**
+ * An array of values from the [[Permissions]] enum. These indicate permissions that this user has granted,
+ * either implicitly or explicitly.
+ */
+ grantedPermissions: Permissions[];
properties: { [name: string]: string };
}
@@ -32,6 +37,7 @@ export class User implements UserLike, Patchable {
private _name: string;
private _properties: { [name: string]: string };
private _groups: GroupMask;
+ private _grantedPermissions: Permissions[];
public get context() { return this._context; }
public get id() { return this._id; }
@@ -68,6 +74,9 @@ export class User implements UserLike, Patchable {
*/
public get properties() { return Object.freeze({ ...this._properties }); }
+ /** @inheritdoc */
+ public get grantedPermissions() { return [...this._grantedPermissions]; }
+
/**
* PUBLIC METHODS
*/
@@ -85,6 +94,7 @@ export class User implements UserLike, Patchable {
return this.internal.prompt(text, acceptInput);
}
+ /** @hidden */
public copy(from: Partial): this {
// Pause change detection while we copy the values into the actor.
const wasObserving = this.internal.observing;
@@ -101,17 +111,20 @@ export class User implements UserLike, Patchable {
this.groups = from.groups;
}
}
+ if (from.grantedPermissions !== undefined) { this._grantedPermissions = from.grantedPermissions; }
this.internal.observing = wasObserving;
return this;
}
+ /** @hidden */
public toJSON() {
return {
id: this.id,
name: this.name,
groups: this.groups.packed(),
properties: this.properties,
+ grantedPermissions: this.grantedPermissions
} as UserLike;
}
diff --git a/packages/sdk/src/user/userInternal.ts b/packages/sdk/src/user/userInternal.ts
index 31ce8a4dc..7d5a628de 100644
--- a/packages/sdk/src/user/userInternal.ts
+++ b/packages/sdk/src/user/userInternal.ts
@@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/
-import { DialogResponse, User, UserLike } from '..';
+import { DialogResponse, Permissions, User, UserLike } from '..';
import { InternalPatchable, Payloads } from '../internal';
import { ContextInternal } from '../core/contextInternal';
@@ -36,7 +36,12 @@ export class UserInternal implements InternalPatchable {
} as Payloads.ShowDialog;
return new Promise((resolve, reject) => {
- this.context.sendPayload(payload, { resolve, reject });
+ if (this.user.grantedPermissions.includes(Permissions.UserInteraction)) {
+ this.context.sendPayload(payload, { resolve, reject });
+ } else {
+ reject(`Permission denied on user ${this.user.id} (${this.user.name}). Either this MRE did not ` +
+ "request the UserInteraction permission, or it was denied by the user.");
+ }
})
.then(response => {
if (response.failureMessage) {
diff --git a/packages/sdk/src/util/manifestSchema.json b/packages/sdk/src/util/manifestSchema.json
new file mode 100644
index 000000000..273e1c085
--- /dev/null
+++ b/packages/sdk/src/util/manifestSchema.json
@@ -0,0 +1,33 @@
+{
+ "$schema": "https://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "author": { "type": "string" },
+ "license": {
+ "description": "An SPDX short license ID",
+ "type": "string"
+ },
+ "repositoryUrl": {
+ "description": "The public repository for this MRE's source code",
+ "type": "string"
+ },
+ "permissions": {
+ "description": "A list of permissions required for this MRE to run",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["user-tracking", "user-interaction"]
+ }
+ },
+ "optionalPermissions": {
+ "description": "A list of permissions that this MRE can use, but are not required",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["user-tracking", "user-interaction"]
+ }
+ }
+ }
+}
diff --git a/packages/sdk/src/util/webHost.ts b/packages/sdk/src/util/webHost.ts
index 5e8333e44..951542ef1 100644
--- a/packages/sdk/src/util/webHost.ts
+++ b/packages/sdk/src/util/webHost.ts
@@ -8,7 +8,14 @@ import * as Restify from 'restify';
import { NotFoundError } from 'restify-errors';
import etag from 'etag';
-import { log, MultipeerAdapter } from '..';
+import { validate } from 'jsonschema';
+import manifestSchema from './manifestSchema.json';
+import { resolve } from 'path';
+import { readFile as _readFile } from 'fs';
+import { promisify } from 'util';
+const readFile = promisify(_readFile);
+
+import { log, MultipeerAdapter, Permissions } from '..';
import { Adapter } from '../internal';
type StaticBufferInfo = {
@@ -24,6 +31,7 @@ export class WebHost {
private _adapter: Adapter;
private _baseDir: string;
private _baseUrl: string;
+ private manifest: Buffer = null;
public get adapter() { return this._adapter; }
public get baseDir() { return this._baseDir; }
@@ -35,6 +43,8 @@ export class WebHost {
baseDir?: string;
baseUrl?: string;
port?: string | number;
+ permissions?: Permissions[];
+ optionalPermissions?: Permissions[];
} = {}) {
const pjson = require('../../package.json'); /* eslint-disable-line @typescript-eslint/no-var-requires */
log.info('app', `Node: ${process.version}`);
@@ -55,25 +65,29 @@ export class WebHost {
this._adapter = new MultipeerAdapter({ port });
// Start listening for new app connections from a multi-peer client.
- this._adapter.listen()
- .then(server => {
- this._baseUrl = this._baseUrl || server.url.replace(/\[::\]/u, '127.0.0.1');
- log.info('app', `${server.name} listening on ${JSON.stringify(server.address())}`);
- log.info('app', `baseUrl: ${this.baseUrl}`);
- log.info('app', `baseDir: ${this.baseDir}`);
- if (this.baseDir) {
- this.serveStaticFiles(server);
- }
- })
- .catch(reason => log.error('app', `Failed to start HTTP server: ${reason}`));
- }
-
- private serveStaticFiles(server: Restify.Server) {
- server.get(`/buffers/:name`,
- this.checkStaticBuffers,
- Restify.plugins.conditionalRequest(),
- this.serveStaticBuffers);
- server.get('/*', Restify.plugins.serveStaticFiles(this._baseDir));
+ this.validateManifest()
+ .then(() => this._adapter.listen())
+ .then(server => {
+ this._baseUrl = this._baseUrl || server.url.replace(/\[::\]/u, '127.0.0.1');
+ log.info('app', `${server.name} listening on ${JSON.stringify(server.address())}`);
+ log.info('app', `baseUrl: ${this.baseUrl}`);
+ log.info('app', `baseDir: ${this.baseDir}`);
+
+ // check if a procedural manifest is needed, and serve if so
+ this.serveManifestIfNeeded(server, options.permissions, options.optionalPermissions);
+
+ // serve static buffers
+ server.get(`/buffers/:name`,
+ this.checkStaticBuffers,
+ Restify.plugins.conditionalRequest(),
+ this.serveStaticBuffers);
+
+ // serve static files
+ if (this.baseDir) {
+ server.get('/*', Restify.plugins.serveStaticFiles(this._baseDir));
+ }
+ })
+ .catch(reason => log.error('app', `Failed to start HTTP server: ${reason}`));
}
private checkStaticBuffers = (req: Restify.Request, res: Restify.Response, next: Restify.Next) => {
@@ -97,6 +111,55 @@ export class WebHost {
}
};
+ private async validateManifest() {
+ const manifestPath = resolve(this.baseDir, './manifest.json');
+
+ try {
+ this.manifest = await readFile(manifestPath);
+ } catch {
+ return;
+ }
+
+ let manifestJson: any;
+ try {
+ manifestJson = JSON.parse(this.manifest.toString('utf8'));
+ } catch {
+ log.error('app', `App manifest "${manifestPath}" is not JSON`);
+ this.manifest = null;
+ return;
+ }
+
+ const result = validate(manifestJson, manifestSchema);
+ if (!result.valid) {
+ log.error('app', `App manifest "${manifestPath}" is not valid:\n${result.errors.join('\n')}`);
+ this.manifest = null;
+ return;
+ }
+ }
+
+ private serveManifestIfNeeded(
+ server: Restify.Server, permissions?: Permissions[], optionalPermissions?: Permissions[]
+ ) {
+ // print warning if no manifest supplied
+ if (!this.manifest && !permissions && !optionalPermissions) {
+ log.warning('app',
+ "No MRE manifest provided, and no permissions requested! For this MRE to use protected features, " +
+ `provide an MRE manifest at "${resolve(this.baseDir, './manifest.json')}", or pass a permissions ` +
+ "list into the WebHost constructor.");
+ }
+
+ server.get('/manifest.json', (_, res, next) => {
+ if (this.manifest) {
+ res.send(200, this.manifest, { "Content-Type": "application/json" });
+ } else if (permissions || optionalPermissions) {
+ res.json(200, { permissions, optionalPermissions });
+ } else {
+ res.send(404);
+ }
+ next();
+ });
+ }
+
/**
* Serve arbitrary binary blobs from a URL
* @param filename A unique string ID for the blob
diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json
index d1254ee34..07523a96f 100644
--- a/packages/sdk/tsconfig.json
+++ b/packages/sdk/tsconfig.json
@@ -1,12 +1,13 @@
{
"extends": "../tsconfig.packages.json",
"include": [
- "./src/**/*.ts", "./types/**/*.d.ts"
+ "./src/**/*.ts", "./src/**/*.json", "./types/**/*.d.ts"
],
"compilerOptions": {
"outDir": "./built", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
- "typeRoots": ["./types"]
+ "typeRoots": ["./types"],
+ "resolveJsonModule": true
},
"references": [
{