feat: move unstable Deno.permissions to navigator.permissions (#6244)

This commit is contained in:
Kitson Kelly 2020-07-09 19:00:18 +10:00 committed by GitHub
parent e92cf5b9e8
commit 202e7fa6ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 403 additions and 243 deletions

View File

@ -19,9 +19,6 @@ export { shutdown, ShutdownMode } from "./net.ts";
export { listen, listenDatagram, connect } from "./net_unstable.ts";
export { startTls } from "./tls.ts";
export { kill } from "./ops/process.ts";
export { permissions, Permissions } from "./permissions.ts";
export { PermissionStatus } from "./permissions.ts";
export type { PermissionName, PermissionState } from "./permissions.ts";
export { DiagnosticCategory } from "./diagnostics.ts";
export type {
Diagnostic,

View File

@ -7,7 +7,6 @@ import * as abortSignal from "./web/abort_signal.ts";
import * as blob from "./web/blob.ts";
import * as consoleTypes from "./web/console.ts";
import * as csprng from "./ops/get_random_values.ts";
import type * as promiseTypes from "./web/promise.ts";
import * as customEvent from "./web/custom_event.ts";
import * as domException from "./web/dom_exception.ts";
import * as domFile from "./web/dom_file.ts";
@ -17,16 +16,19 @@ import * as eventTarget from "./web/event_target.ts";
import * as formData from "./web/form_data.ts";
import * as fetchTypes from "./web/fetch.ts";
import * as headers from "./web/headers.ts";
import * as navigator from "./web/navigator.ts";
import * as permissions from "./web/permissions.ts";
import * as performanceUtil from "./web/performance.ts";
import type * as promiseTypes from "./web/promise.ts";
import * as queuingStrategy from "./web/streams/queuing_strategy.ts";
import * as readableStream from "./web/streams/readable_stream.ts";
import * as request from "./web/request.ts";
import * as textEncoding from "./web/text_encoding.ts";
import * as timers from "./web/timers.ts";
import * as transformStream from "./web/streams/transform_stream.ts";
import * as url from "./web/url.ts";
import * as urlSearchParams from "./web/url_search_params.ts";
import * as workers from "./web/workers.ts";
import * as performanceUtil from "./web/performance.ts";
import * as request from "./web/request.ts";
import * as readableStream from "./web/streams/readable_stream.ts";
import * as transformStream from "./web/streams/transform_stream.ts";
import * as queuingStrategy from "./web/streams/queuing_strategy.ts";
import * as writableStream from "./web/streams/writable_stream.ts";
// These imports are not exposed and therefore are fine to just import the
@ -221,24 +223,28 @@ export const windowOrWorkerGlobalScopeProperties = {
queuingStrategy.ByteLengthQueuingStrategyImpl
),
CountQueuingStrategy: nonEnumerable(queuingStrategy.CountQueuingStrategyImpl),
crypto: readOnly(csprng),
File: nonEnumerable(domFile.DomFileImpl),
CustomEvent: nonEnumerable(customEvent.CustomEventImpl),
crypto: readOnly(csprng),
DOMException: nonEnumerable(domException.DOMExceptionImpl),
ErrorEvent: nonEnumerable(errorEvent.ErrorEventImpl),
Event: nonEnumerable(event.EventImpl),
EventTarget: nonEnumerable(eventTarget.EventTargetImpl),
URL: nonEnumerable(url.URLImpl),
URLSearchParams: nonEnumerable(urlSearchParams.URLSearchParamsImpl),
Headers: nonEnumerable(headers.HeadersImpl),
File: nonEnumerable(domFile.DomFileImpl),
FormData: nonEnumerable(formData.FormDataImpl),
TextEncoder: nonEnumerable(textEncoding.TextEncoder),
TextDecoder: nonEnumerable(textEncoding.TextDecoder),
Headers: nonEnumerable(headers.HeadersImpl),
navigator: nonEnumerable(new navigator.NavigatorImpl()),
Navigator: nonEnumerable(navigator.NavigatorImpl),
performance: writable(new performanceUtil.Performance()),
Permissions: nonEnumerable(permissions.PermissionsImpl),
PermissionStatus: nonEnumerable(permissions.PermissionStatusImpl),
ReadableStream: nonEnumerable(readableStream.ReadableStreamImpl),
TransformStream: nonEnumerable(transformStream.TransformStreamImpl),
Request: nonEnumerable(request.Request),
Response: nonEnumerable(fetchTypes.Response),
performance: writable(new performanceUtil.Performance()),
TextDecoder: nonEnumerable(textEncoding.TextDecoder),
TextEncoder: nonEnumerable(textEncoding.TextEncoder),
TransformStream: nonEnumerable(transformStream.TransformStreamImpl),
URL: nonEnumerable(url.URLImpl),
URLSearchParams: nonEnumerable(urlSearchParams.URLSearchParamsImpl),
Worker: nonEnumerable(workers.WorkerImpl),
WritableStream: nonEnumerable(writableStream.WritableStreamImpl),
};

View File

@ -19,6 +19,40 @@ declare interface ImportMeta {
main: boolean;
}
declare interface Permissions {
/** Resolves to the current status of a permission.
*
* ```ts
* const status = await navigator.permissions.query({ name: "read", path: "/etc" });
* if (status.state === "granted") {
* data = await Deno.readFile("/etc/passwd");
* }
* ```
*/
query(permissionDesc: Deno.PermissionDescriptor): Promise<PermissionStatus>;
/** Requests the permission, and resolves to the state of the permission.
*
* ```ts
* const status = await navigator.permissions.request({ name: "env" });
* if (status.state === "granted") {
* console.log(Deno.dir("home");
* } else {
* console.log("'env' permission is denied.");
* }
* ```
*/
request(permissionDesc: Deno.PermissionDescriptor): Promise<PermissionStatus>;
/** Revokes a permission, and resolves to the state of the permission.
*
* ```ts
* const status = await Deno.revoke({ name: "run" });
* ```
*/
revoke(permissionDesc: Deno.PermissionDescriptor): Promise<PermissionStatus>;
}
declare namespace Deno {
/** A set of error constructors that are raised by Deno APIs. */
export const errors: {
@ -1897,6 +1931,65 @@ declare namespace Deno {
* Requires `allow-run` permission. */
export function run<T extends RunOptions = RunOptions>(opt: T): Process<T>;
/** The name of a "powerful feature" which needs permission.
*
* See: https://w3c.github.io/permissions/#permission-registry
*
* Note that the definition of `PermissionName` in the above spec is swapped
* out for a set of Deno permissions which are not web-compatible. */
export type PermissionName =
| "run"
| "read"
| "write"
| "net"
| "env"
| "plugin"
| "hrtime";
export interface RunPermissionDescriptor {
name: "run";
}
export interface ReadPermissionDescriptor {
name: "read";
path?: string;
}
export interface WritePermissionDescriptor {
name: "write";
path?: string;
}
export interface NetPermissionDescriptor {
name: "net";
url?: string;
}
export interface EnvPermissionDescriptor {
name: "env";
}
export interface PluginPermissionDescriptor {
name: "plugin";
}
export interface HrtimePermissionDescriptor {
name: "hrtime";
}
/** Permission descriptors which define a permission and can be queried,
* requested, or revoked.
*
* See: https://w3c.github.io/permissions/#permission-descriptor */
export type PermissionDescriptor =
| RunPermissionDescriptor
| ReadPermissionDescriptor
| WritePermissionDescriptor
| NetPermissionDescriptor
| EnvPermissionDescriptor
| PluginPermissionDescriptor
| HrtimePermissionDescriptor;
interface InspectOptions {
depth?: number;
}

View File

@ -1556,6 +1556,60 @@ declare const AbortSignal: {
new (): AbortSignal;
};
type PermissionState = "denied" | "granted" | "prompt";
interface PermissionStatusEventMap {
change: Event;
}
interface PermissionStatus extends EventTarget {
onchange: ((this: PermissionStatus, ev: Event) => any) | null;
readonly state: PermissionState;
addEventListener<K extends keyof PermissionStatusEventMap>(
type: K,
listener: (this: PermissionStatus, ev: PermissionStatusEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void;
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
): void;
removeEventListener<K extends keyof PermissionStatusEventMap>(
type: K,
listener: (this: PermissionStatus, ev: PermissionStatusEventMap[K]) => any,
options?: boolean | EventListenerOptions
): void;
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions
): void;
}
/** Deno does not currently support any of the browser permissions, and so the
* `name` property of the global types is `undefined`. The Deno permissions
* that are supported are defined in the `lib.deno.ns.d.ts` and pull from the
* `Deno` namespace. */
declare interface PermissionDescriptor {
name: undefined;
}
declare interface Permissions {
query(permissionDesc: PermissionDescriptor): Promise<PermissionStatus>;
}
declare const Permissions: {
prototype: Permissions;
new (): Permissions;
};
declare class Navigator {
readonly permissions: Permissions;
}
declare const navigator: Navigator;
interface ErrorConstructor {
/** See https://v8.dev/docs/stack-trace-api#stack-trace-collection-for-custom-exceptions. */
// eslint-disable-next-line @typescript-eslint/ban-types

View File

@ -956,120 +956,6 @@ declare namespace Deno {
* Requires `allow-run` permission. */
export function kill(pid: number, signo: number): void;
/** The name of a "powerful feature" which needs permission.
*
* See: https://w3c.github.io/permissions/#permission-registry
*
* Note that the definition of `PermissionName` in the above spec is swapped
* out for a set of Deno permissions which are not web-compatible. */
export type PermissionName =
| "run"
| "read"
| "write"
| "net"
| "env"
| "plugin"
| "hrtime";
/** The current status of the permission.
*
* See: https://w3c.github.io/permissions/#status-of-a-permission */
export type PermissionState = "granted" | "denied" | "prompt";
export interface RunPermissionDescriptor {
name: "run";
}
export interface ReadPermissionDescriptor {
name: "read";
path?: string;
}
export interface WritePermissionDescriptor {
name: "write";
path?: string;
}
export interface NetPermissionDescriptor {
name: "net";
/** Optional url associated with this descriptor.
*
* If specified: must be a valid url. Expected format: <scheme>://<host_or_ip>[:port][/path]
* If the scheme is unknown, callers should specify some scheme, such as x:// na:// unknown://
*
* See: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml */
url?: string;
}
export interface EnvPermissionDescriptor {
name: "env";
}
export interface PluginPermissionDescriptor {
name: "plugin";
}
export interface HrtimePermissionDescriptor {
name: "hrtime";
}
/** Permission descriptors which define a permission and can be queried,
* requested, or revoked.
*
* See: https://w3c.github.io/permissions/#permission-descriptor */
export type PermissionDescriptor =
| RunPermissionDescriptor
| ReadPermissionDescriptor
| WritePermissionDescriptor
| NetPermissionDescriptor
| EnvPermissionDescriptor
| PluginPermissionDescriptor
| HrtimePermissionDescriptor;
export class Permissions {
/** Resolves to the current status of a permission.
*
* ```ts
* const status = await Deno.permissions.query({ name: "read", path: "/etc" });
* if (status.state === "granted") {
* data = await Deno.readFile("/etc/passwd");
* }
* ```
*/
query(desc: PermissionDescriptor): Promise<PermissionStatus>;
/** Revokes a permission, and resolves to the state of the permission.
*
* const status = await Deno.permissions.revoke({ name: "run" });
* assert(status.state !== "granted")
*/
revoke(desc: PermissionDescriptor): Promise<PermissionStatus>;
/** Requests the permission, and resolves to the state of the permission.
*
* ```ts
* const status = await Deno.permissions.request({ name: "env" });
* if (status.state === "granted") {
* console.log(Deno.dir("home");
* } else {
* console.log("'env' permission is denied.");
* }
* ```
*/
request(desc: PermissionDescriptor): Promise<PermissionStatus>;
}
/** **UNSTABLE**: Under consideration to move to `navigator.permissions` to
* match web API. It could look like `navigator.permissions.query({ name: Deno.symbols.read })`.
*/
export const permissions: Permissions;
/** see: https://w3c.github.io/permissions/#permissionstatus */
export class PermissionStatus {
state: PermissionState;
constructor(state: PermissionState);
}
/** **UNSTABLE**: New API, yet to be vetted. Additional consideration is still
* necessary around the permissions required.
*

View File

@ -1,7 +1,6 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { sendSync } from "./dispatch_json.ts";
import type { PermissionState } from "../permissions.ts";
interface PermissionRequest {
name: string;

View File

@ -1,79 +0,0 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import * as permissionsOps from "./ops/permissions.ts";
export type PermissionName =
| "read"
| "write"
| "net"
| "env"
| "run"
| "plugin"
| "hrtime";
// NOTE: Keep in sync with cli/permissions.rs
export type PermissionState = "granted" | "denied" | "prompt";
export interface RunPermissionDescriptor {
name: "run";
}
export interface ReadPermissionDescriptor {
name: "read";
path?: string;
}
export interface WritePermissionDescriptor {
name: "write";
path?: string;
}
export interface NetPermissionDescriptor {
name: "net";
url?: string;
}
export interface EnvPermissionDescriptor {
name: "env";
}
export interface PluginPermissionDescriptor {
name: "plugin";
}
export interface HrtimePermissionDescriptor {
name: "hrtime";
}
export type PermissionDescriptor =
| RunPermissionDescriptor
| ReadPermissionDescriptor
| WritePermissionDescriptor
| NetPermissionDescriptor
| EnvPermissionDescriptor
| PluginPermissionDescriptor
| HrtimePermissionDescriptor;
export class PermissionStatus {
constructor(public state: PermissionState) {}
// TODO(kt3k): implement onchange handler
}
export class Permissions {
query(desc: PermissionDescriptor): Promise<PermissionStatus> {
const state = permissionsOps.query(desc);
return Promise.resolve(new PermissionStatus(state));
}
revoke(desc: PermissionDescriptor): Promise<PermissionStatus> {
const state = permissionsOps.revoke(desc);
return Promise.resolve(new PermissionStatus(state));
}
request(desc: PermissionDescriptor): Promise<PermissionStatus> {
const state = permissionsOps.request(desc);
return Promise.resolve(new PermissionStatus(state));
}
}
export const permissions = new Permissions();

12
cli/js/web/navigator.ts Normal file
View File

@ -0,0 +1,12 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { PermissionsImpl as Permissions } from "./permissions.ts";
export class NavigatorImpl implements Navigator {
permissions = new Permissions();
}
Object.defineProperty(NavigatorImpl, "name", {
value: "Navigator",
configurable: true,
});

181
cli/js/web/permissions.ts Normal file
View File

@ -0,0 +1,181 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import * as permissionsOps from "../ops/permissions.ts";
import { EventTargetImpl as EventTarget } from "./event_target.ts";
const permissionNames = [
"read",
"write",
"net",
"env",
"run",
"plugin",
"hrtime",
] as const;
type PermissionName = typeof permissionNames[number];
interface RunPermissionDescriptor {
name: "run";
}
interface ReadPermissionDescriptor {
name: "read";
path?: string;
}
interface WritePermissionDescriptor {
name: "write";
path?: string;
}
interface NetPermissionDescriptor {
name: "net";
url?: string;
}
interface EnvPermissionDescriptor {
name: "env";
}
interface PluginPermissionDescriptor {
name: "plugin";
}
interface HrtimePermissionDescriptor {
name: "hrtime";
}
type DenoPermissionDescriptor =
| RunPermissionDescriptor
| ReadPermissionDescriptor
| WritePermissionDescriptor
| NetPermissionDescriptor
| EnvPermissionDescriptor
| PluginPermissionDescriptor
| HrtimePermissionDescriptor;
interface StatusCacheValue {
state: PermissionState;
status: PermissionStatusImpl;
}
export class PermissionStatusImpl extends EventTarget
implements PermissionStatus {
#state: { state: PermissionState };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onchange: ((this: PermissionStatus, event: Event) => any) | null = null;
get state(): PermissionState {
return this.#state.state;
}
constructor(state: { state: PermissionState }) {
super();
this.#state = state;
}
dispatchEvent(event: Event): boolean {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let dispatched = super.dispatchEvent(event as any);
if (dispatched && this.onchange) {
this.onchange.call(this, event);
dispatched = !event.defaultPrevented;
}
return dispatched;
}
get [Symbol.toStringTag](): string {
return "PermissionStatus";
}
}
/** A cache of `PermissionStatus` objects and their last known state. */
const statusCache = new Map<string, StatusCacheValue>();
/** Cache the state of a descriptor and return its `PermissionStatus`. */
function cache(
desc: DenoPermissionDescriptor,
state: PermissionState
): PermissionStatusImpl {
let key = desc.name;
if ((desc.name === "read" || desc.name === "write") && desc.path) {
key += `-${desc.path}`;
} else if (desc.name === "net" && desc.url) {
key += `-${desc.url}`;
}
if (statusCache.has(key)) {
const status = statusCache.get(key)!;
if (status.state !== state) {
status.state = state;
status.status.dispatchEvent(new Event("change", { cancelable: false }));
}
return status.status;
}
const status: { state: PermissionState; status?: PermissionStatusImpl } = {
state,
};
status.status = new PermissionStatusImpl(status);
statusCache.set(key, status as StatusCacheValue);
return status.status;
}
function isValidDescriptor(
desc: PermissionDescriptor | DenoPermissionDescriptor
): desc is DenoPermissionDescriptor {
return permissionNames.includes(desc.name as PermissionName);
}
export class PermissionsImpl implements Permissions {
query(
desc: PermissionDescriptor | DenoPermissionDescriptor
): Promise<PermissionStatus> {
if (!isValidDescriptor(desc)) {
return Promise.reject(
new TypeError(
`The provided value "${desc.name}" is not a valid permission name.`
)
);
}
const state = permissionsOps.query(desc);
return Promise.resolve(cache(desc, state) as PermissionStatus);
}
revoke(
desc: PermissionDescriptor | DenoPermissionDescriptor
): Promise<PermissionStatus> {
if (!isValidDescriptor(desc)) {
return Promise.reject(
new TypeError(
`The provided value "${desc.name}" is not a valid permission name.`
)
);
}
const state = permissionsOps.revoke(desc);
return Promise.resolve(cache(desc, state) as PermissionStatus);
}
request(
desc: PermissionDescriptor | DenoPermissionDescriptor
): Promise<PermissionStatus> {
if (!isValidDescriptor(desc)) {
return Promise.reject(
new TypeError(
`The provided value "${desc.name}" is not a valid permission name.`
)
);
}
const state = permissionsOps.request(desc);
return Promise.resolve(cache(desc, state) as PermissionStatus);
}
}
Object.defineProperty(PermissionStatusImpl, "name", {
value: "PermissionStatus",
configurable: true,
});
Object.defineProperty(PermissionsImpl, "name", {
value: "Permissions",
configurable: true,
});

View File

@ -1,5 +1,5 @@
window.onload = async (): Promise<void> => {
console.log(performance.now() % 2 !== 0);
await Deno.permissions.revoke({ name: "hrtime" });
await navigator.permissions.revoke({ name: "hrtime" });
console.log(performance.now() % 2 === 0);
};

View File

@ -18,11 +18,11 @@ export function assert(cond: unknown): asserts cond {
function genFunc(grant: Deno.PermissionName): [string, () => Promise<void>] {
const gen: () => Promise<void> = async function Granted(): Promise<void> {
const status0 = await Deno.permissions.query({ name: grant });
const status0 = await navigator.permissions.query({ name: grant });
assert(status0 != null);
assert(status0.state === "granted");
const status1 = await Deno.permissions.revoke({ name: grant });
const status1 = await navigator.permissions.revoke({ name: grant });
assert(status1 != null);
assert(status1.state === "prompt");
};

View File

@ -1,27 +1,40 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { unitTest, assert } from "./test_util.ts";
import { unitTest, assert, assertThrowsAsync } from "./test_util.ts";
unitTest(async function permissionInvalidName(): Promise<void> {
let thrown = false;
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await Deno.permissions.query({ name: "foo" as any });
} catch (e) {
thrown = true;
assert(e instanceof Error);
} finally {
assert(thrown);
}
await assertThrowsAsync(async () => {
// @ts-expect-error name should not accept "foo"
await navigator.permissions.query({ name: "foo" });
}, TypeError);
});
unitTest(async function permissionNetInvalidUrl(): Promise<void> {
let thrown = false;
try {
await Deno.permissions.query({ name: "net", url: ":" });
} catch (e) {
thrown = true;
assert(e instanceof URIError);
} finally {
assert(thrown);
}
await assertThrowsAsync(async () => {
await navigator.permissions.query({ name: "net", url: ":" });
}, URIError);
});
unitTest(async function permissionQueryReturnsEventTarget(): Promise<void> {
const status = await navigator.permissions.query({ name: "hrtime" });
assert(["granted", "denied", "prompt"].includes(status.state));
let called = false;
status.addEventListener("change", () => {
called = true;
});
status.dispatchEvent(new Event("change"));
assert(called);
assert(status === (await navigator.permissions.query({ name: "hrtime" })));
});
unitTest(async function permissionQueryForReadReturnsSameStatus() {
const status1 = await navigator.permissions.query({
name: "read",
path: ".",
});
const status2 = await navigator.permissions.query({
name: "read",
path: ".",
});
assert(status1 === status2);
});

View File

@ -42,7 +42,7 @@ export function fmtPerms(perms: Permissions): string {
}
const isGranted = async (name: Deno.PermissionName): Promise<boolean> =>
(await Deno.permissions.query({ name })).state === "granted";
(await navigator.permissions.query({ name })).state === "granted";
export async function getProcessPermissions(): Promise<Permissions> {
return {

View File

@ -49,7 +49,7 @@ async function dropWorkerPermissions(
});
for (const perm of permsToDrop) {
await Deno.permissions.revoke({ name: perm });
await navigator.permissions.revoke({ name: perm });
}
}

View File

@ -1,7 +1,5 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
const { PermissionDenied } = Deno.errors;
function getPermissionString(descriptors: Deno.PermissionDescriptor[]): string {
return descriptors.length
? ` ${descriptors
@ -63,9 +61,9 @@ export async function grant(
? descriptor
: [descriptor, ...descriptors];
for (const descriptor of descriptors) {
let state = (await Deno.permissions.query(descriptor)).state;
let state = (await navigator.permissions.query(descriptor)).state;
if (state === "prompt") {
state = (await Deno.permissions.request(descriptor)).state;
state = (await navigator.permissions.request(descriptor)).state;
}
if (state === "granted") {
result.push(descriptor);
@ -105,13 +103,13 @@ export async function grantOrThrow(
? descriptor
: [descriptor, ...descriptors];
for (const descriptor of descriptors) {
const { state } = await Deno.permissions.request(descriptor);
const { state } = await navigator.permissions.request(descriptor);
if (state !== "granted") {
denied.push(descriptor);
}
}
if (denied.length) {
throw new PermissionDenied(
throw new Deno.errors.PermissionDenied(
`The following permissions have not been granted:\n${getPermissionString(
denied
)}`