@flighthq/permissions 0.3.0-next.906.07cea63 → 0.3.1-next.209.43ef1bc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/enablePermissionGuards.d.ts +4 -0
- package/dist/enablePermissionGuards.d.ts.map +1 -0
- package/dist/enablePermissionGuards.js +29 -0
- package/dist/enablePermissionGuards.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/permission.d.ts +4 -1
- package/dist/permission.d.ts.map +1 -1
- package/dist/permission.js +90 -22
- package/dist/permission.js.map +1 -1
- package/package.json +5 -2
- package/src/enablePermissionGuards.test.ts +83 -0
- package/src/permission.test.ts +140 -1
package/dist/contract.d.ts
CHANGED
package/dist/contract.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC"}
|
package/dist/contract.js
CHANGED
package/dist/contract.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enablePermissionGuards.d.ts","sourceRoot":"","sources":["../src/enablePermissionGuards.ts"],"names":[],"mappings":"AAMA,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAED,wBAAgB,uBAAuB,IAAI,IAAI,CAG9C;AAMD,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { logOnce } from '@flighthq/log/contract';
|
|
2
|
+
import { LogLevel } from '@flighthq/types/contract';
|
|
3
|
+
import { setPermissionRequestFallbackGuard } from './permission';
|
|
4
|
+
export function arePermissionGuardsEnabled() {
|
|
5
|
+
return _enabled;
|
|
6
|
+
}
|
|
7
|
+
export function disablePermissionGuards() {
|
|
8
|
+
setPermissionRequestFallbackGuard(null);
|
|
9
|
+
_enabled = false;
|
|
10
|
+
}
|
|
11
|
+
// Installs diagnostics for the one silent degradation in this package: `requestPermission` on a name with
|
|
12
|
+
// no concrete request path falls back to a plain state query. Nothing throws, a plausible state comes back,
|
|
13
|
+
// and no OS prompt ever appears — so a caller waiting on a prompt waits forever with no signal that the
|
|
14
|
+
// name has no request route on this platform.
|
|
15
|
+
export function enablePermissionGuards() {
|
|
16
|
+
setPermissionRequestFallbackGuard(warnPermissionRequestFallback);
|
|
17
|
+
_enabled = true;
|
|
18
|
+
}
|
|
19
|
+
function warnPermissionRequestFallback(name, state) {
|
|
20
|
+
// Keyed per name: a second name with no request path is a different observation worth reporting, while
|
|
21
|
+
// the same name asked every frame is not.
|
|
22
|
+
logOnce(`permissions:request-fallback:${name}`, LogLevel.Warn, {
|
|
23
|
+
message: `requestPermission('${name}'): no request path for this name on this backend, so it fell back to a plain state query and NO prompt was shown; the resulting '${state}' is a read, not a decision`,
|
|
24
|
+
name,
|
|
25
|
+
state,
|
|
26
|
+
}, 'permissions');
|
|
27
|
+
}
|
|
28
|
+
let _enabled = false;
|
|
29
|
+
//# sourceMappingURL=enablePermissionGuards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enablePermissionGuards.js","sourceRoot":"","sources":["../src/enablePermissionGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,iCAAiC,EAAE,MAAM,cAAc,CAAC;AAEjE,MAAM,UAAU,0BAA0B;IACxC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,uBAAuB;IACrC,iCAAiC,CAAC,IAAI,CAAC,CAAC;IACxC,QAAQ,GAAG,KAAK,CAAC;AACnB,CAAC;AAED,0GAA0G;AAC1G,4GAA4G;AAC5G,wGAAwG;AACxG,8CAA8C;AAC9C,MAAM,UAAU,sBAAsB;IACpC,iCAAiC,CAAC,6BAA6B,CAAC,CAAC;IACjE,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC;AAED,SAAS,6BAA6B,CAAC,IAAoB,EAAE,KAAsB;IACjF,uGAAuG;IACvG,0CAA0C;IAC1C,OAAO,CACL,gCAAgC,IAAI,EAAE,EACtC,QAAQ,CAAC,IAAI,EACb;QACE,OAAO,EAAE,sBAAsB,IAAI,qIAAqI,KAAK,6BAA6B;QAC1M,IAAI;QACJ,KAAK;KACN,EACD,aAAa,CACd,CAAC;AACJ,CAAC;AAED,IAAI,QAAQ,GAAG,KAAK,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { getPermissionState, requestPermission } from './contract';
|
|
1
|
+
export { disablePermissionGuards, enablePermissionGuards, explainPermissionState, getPermissionState, getPermissionStates, requestPermission, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { getPermissionState, requestPermission } from './contract';
|
|
1
|
+
export { disablePermissionGuards, enablePermissionGuards, explainPermissionState, getPermissionState, getPermissionStates, requestPermission, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
|
package/dist/permission.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { PermissionBackend, PermissionName, PermissionState } from '@flighthq/types/contract';
|
|
1
|
+
import type { PermissionBackend, PermissionName, PermissionRequestFallbackGuard, PermissionState, PermissionStateExplanation } from '@flighthq/types/contract';
|
|
2
2
|
export declare function createWebPermissionBackend(): PermissionBackend;
|
|
3
|
+
export declare function explainPermissionState(name: PermissionName): Promise<PermissionStateExplanation>;
|
|
3
4
|
export declare function getPermissionBackend(): PermissionBackend;
|
|
4
5
|
export declare function getPermissionState(name: PermissionName): Promise<PermissionState>;
|
|
6
|
+
export declare function getPermissionStates(names: readonly PermissionName[]): Promise<PermissionState[]>;
|
|
5
7
|
export declare function requestPermission(name: PermissionName): Promise<PermissionState>;
|
|
6
8
|
export declare function setPermissionBackend(backend: PermissionBackend | null): void;
|
|
9
|
+
export declare function setPermissionRequestFallbackGuard(guard: PermissionRequestFallbackGuard | null): void;
|
|
7
10
|
//# sourceMappingURL=permission.d.ts.map
|
package/dist/permission.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACd,8BAA8B,EAC9B,eAAe,EACf,0BAA0B,EAC3B,MAAM,0BAA0B,CAAC;AAOlC,wBAAgB,0BAA0B,IAAI,iBAAiB,CAS9D;AAUD,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAiBtG;AAGD,wBAAgB,oBAAoB,IAAI,iBAAiB,CAMxD;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAEjF;AAMD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAEhG;AAKD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAEhF;AAGD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAE5E;AAKD,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,8BAA8B,GAAG,IAAI,GAAG,IAAI,CAEpG"}
|
package/dist/permission.js
CHANGED
|
@@ -13,10 +13,39 @@ export function createWebPermissionBackend() {
|
|
|
13
13
|
},
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
// Reports WHY a state read came out the way it did, for the caller who got 'prompt' and cannot act on it.
|
|
17
|
+
// That one value means three different things — undecided, unqueryable name, or no Permissions API at all —
|
|
18
|
+
// and they have opposite remedies. Runs the same read as getPermissionState and reports which branch the
|
|
19
|
+
// web backend took, so it is a companion probe rather than a second source of truth.
|
|
20
|
+
//
|
|
21
|
+
// A custom backend has no branches to report: its answer is whatever it returned, described as 'decided'
|
|
22
|
+
// for a definite state and 'undecided' for 'prompt'. Only the built-in web backend can distinguish
|
|
23
|
+
// 'unqueryable' from 'unsupported', because only it knows whether navigator.permissions exists.
|
|
24
|
+
export async function explainPermissionState(name) {
|
|
25
|
+
const backend = getPermissionBackend();
|
|
26
|
+
if (backend !== _webBackend) {
|
|
27
|
+
const state = await backend.getState(name);
|
|
28
|
+
return { name, source: state === 'prompt' ? 'undecided' : 'decided', state };
|
|
29
|
+
}
|
|
30
|
+
const permissions = getWebPermissions();
|
|
31
|
+
if (permissions === null) {
|
|
32
|
+
return { name, source: 'unsupported', state: readWebFallbackPermissionState(name) };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const status = await permissions.query({ name });
|
|
36
|
+
const state = status.state;
|
|
37
|
+
return { name, source: state === 'prompt' ? 'undecided' : 'decided', state };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return { name, source: 'unqueryable', state: readWebFallbackPermissionState(name) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
16
43
|
// The active permission backend, lazily defaulting to the web backend. There is always a backend.
|
|
17
44
|
export function getPermissionBackend() {
|
|
18
|
-
if (_backend === null)
|
|
45
|
+
if (_backend === null) {
|
|
19
46
|
_backend = createWebPermissionBackend();
|
|
47
|
+
_webBackend = _backend;
|
|
48
|
+
}
|
|
20
49
|
return _backend;
|
|
21
50
|
}
|
|
22
51
|
// Resolves the current state of a named permission without prompting. Returns 'granted', 'denied',
|
|
@@ -24,6 +53,13 @@ export function getPermissionBackend() {
|
|
|
24
53
|
export function getPermissionState(name) {
|
|
25
54
|
return getPermissionBackend().getState(name);
|
|
26
55
|
}
|
|
56
|
+
// Batch form of getPermissionState. Results are returned in INPUT ORDER as a parallel array rather than a
|
|
57
|
+
// keyed record: the caller may legitimately pass the same name twice, and an array preserves that where a
|
|
58
|
+
// record would silently collapse it. Queries run concurrently; a state read never throws, so neither does
|
|
59
|
+
// this.
|
|
60
|
+
export function getPermissionStates(names) {
|
|
61
|
+
return Promise.all(names.map((name) => getPermissionState(name)));
|
|
62
|
+
}
|
|
27
63
|
// Requests a named permission, triggering the OS prompt where the platform supports it. Resolves to
|
|
28
64
|
// the resulting state; a name with no request path falls back to a plain state query, and a missing
|
|
29
65
|
// API resolves to a sentinel rather than throwing.
|
|
@@ -34,12 +70,17 @@ export function requestPermission(name) {
|
|
|
34
70
|
export function setPermissionBackend(backend) {
|
|
35
71
|
_backend = backend;
|
|
36
72
|
}
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
function
|
|
41
|
-
|
|
73
|
+
// Diagnostics seam for the silent degradation in requestPermission: a name with no concrete request path
|
|
74
|
+
// resolves to a plain state query, so no prompt ever appears and the caller cannot tell. Core stays
|
|
75
|
+
// message-free; enablePermissionGuards installs the reporter.
|
|
76
|
+
export function setPermissionRequestFallbackGuard(guard) {
|
|
77
|
+
_requestFallbackGuard = guard;
|
|
42
78
|
}
|
|
79
|
+
let _backend = null;
|
|
80
|
+
let _requestFallbackGuard = null;
|
|
81
|
+
// Identity of the lazily-created default, so explainPermissionState can tell whether the active backend is
|
|
82
|
+
// the web one whose branches it knows how to describe.
|
|
83
|
+
let _webBackend = null;
|
|
43
84
|
// The per-name fallback state when the Permissions API is absent or the name is unqueryable.
|
|
44
85
|
// notifications is still readable synchronously via Notification.permission ('default' → 'prompt');
|
|
45
86
|
// every other name resolves to the 'prompt' sentinel.
|
|
@@ -55,9 +96,7 @@ async function readWebPermissionState(name) {
|
|
|
55
96
|
const permissions = getWebPermissions();
|
|
56
97
|
if (permissions !== null) {
|
|
57
98
|
try {
|
|
58
|
-
const status = await permissions.query({
|
|
59
|
-
name: getPermissionQueryDescriptorName(name),
|
|
60
|
-
});
|
|
99
|
+
const status = await permissions.query({ name });
|
|
61
100
|
return status.state;
|
|
62
101
|
}
|
|
63
102
|
catch {
|
|
@@ -109,6 +148,23 @@ async function requestWebNotificationPermission() {
|
|
|
109
148
|
return 'prompt';
|
|
110
149
|
}
|
|
111
150
|
}
|
|
151
|
+
// Observes a MIDI access grant purely for the prompt; the access object is discarded, mirroring the
|
|
152
|
+
// discarded geolocation position. sysex is not requested — that is a strictly larger prompt than the
|
|
153
|
+
// 'midi' permission this name denotes.
|
|
154
|
+
async function requestWebMidiPermission() {
|
|
155
|
+
if (typeof navigator === 'undefined')
|
|
156
|
+
return 'prompt';
|
|
157
|
+
const request = navigator.requestMIDIAccess;
|
|
158
|
+
if (typeof request !== 'function')
|
|
159
|
+
return 'prompt';
|
|
160
|
+
try {
|
|
161
|
+
await request.call(navigator);
|
|
162
|
+
return 'granted';
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return 'denied';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
112
168
|
async function requestWebPersistentStoragePermission() {
|
|
113
169
|
const storage = getWebStorageManager();
|
|
114
170
|
if (storage === null || typeof storage.persist !== 'function')
|
|
@@ -120,11 +176,33 @@ async function requestWebPersistentStoragePermission() {
|
|
|
120
176
|
return 'prompt';
|
|
121
177
|
}
|
|
122
178
|
}
|
|
179
|
+
// Takes a screen wake lock and releases it immediately — the prompt is the only thing wanted, the same
|
|
180
|
+
// shape as getUserMedia's stop-tracks. Holding the lock would be a lasting side effect the caller did not
|
|
181
|
+
// ask for; releasing keeps the trigger observation-only.
|
|
182
|
+
async function requestWebScreenWakeLockPermission() {
|
|
183
|
+
if (typeof navigator === 'undefined')
|
|
184
|
+
return 'prompt';
|
|
185
|
+
const wakeLock = navigator
|
|
186
|
+
.wakeLock;
|
|
187
|
+
if (wakeLock === undefined || typeof wakeLock.request !== 'function')
|
|
188
|
+
return 'prompt';
|
|
189
|
+
try {
|
|
190
|
+
const sentinel = await wakeLock.request('screen');
|
|
191
|
+
if (typeof sentinel?.release === 'function')
|
|
192
|
+
await sentinel.release();
|
|
193
|
+
return 'granted';
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return 'denied';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
123
199
|
async function requestWebPermission(name) {
|
|
124
200
|
const router = _permissionRequestRouters[name];
|
|
125
201
|
if (router !== undefined)
|
|
126
202
|
return await router();
|
|
127
|
-
|
|
203
|
+
const state = await readWebPermissionState(name);
|
|
204
|
+
_requestFallbackGuard?.(name, state);
|
|
205
|
+
return state;
|
|
128
206
|
}
|
|
129
207
|
// Stops every track of a granted media stream, releasing the device. Guards a missing getTracks so a
|
|
130
208
|
// stubbed or partial stream never throws.
|
|
@@ -168,23 +246,13 @@ function getWebStorageManager() {
|
|
|
168
246
|
return null;
|
|
169
247
|
return navigator.storage ?? null;
|
|
170
248
|
}
|
|
171
|
-
const _permissionQueryDescriptors = {
|
|
172
|
-
camera: 'camera',
|
|
173
|
-
microphone: 'microphone',
|
|
174
|
-
geolocation: 'geolocation',
|
|
175
|
-
notifications: 'notifications',
|
|
176
|
-
'clipboard-read': 'clipboard-read',
|
|
177
|
-
'clipboard-write': 'clipboard-write',
|
|
178
|
-
'persistent-storage': 'persistent-storage',
|
|
179
|
-
push: 'push',
|
|
180
|
-
midi: 'midi',
|
|
181
|
-
'screen-wake-lock': 'screen-wake-lock',
|
|
182
|
-
};
|
|
183
249
|
const _permissionRequestRouters = {
|
|
184
250
|
camera: () => requestWebMediaPermission('video'),
|
|
185
251
|
microphone: () => requestWebMediaPermission('audio'),
|
|
186
252
|
geolocation: requestWebGeolocationPermission,
|
|
187
253
|
notifications: requestWebNotificationPermission,
|
|
188
254
|
'persistent-storage': requestWebPersistentStoragePermission,
|
|
255
|
+
midi: requestWebMidiPermission,
|
|
256
|
+
'screen-wake-lock': requestWebScreenWakeLockPermission,
|
|
189
257
|
};
|
|
190
258
|
//# sourceMappingURL=permission.js.map
|
package/dist/permission.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permission.js","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"permission.js","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/permissions",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1-next.209.43ef1bc",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
8
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -36,7 +38,8 @@
|
|
|
36
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@flighthq/
|
|
41
|
+
"@flighthq/log": "0.3.1-next.209.43ef1bc",
|
|
42
|
+
"@flighthq/types": "0.3.1-next.209.43ef1bc"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
42
45
|
"typescript": "^5.3.0"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import type { LogEntry } from '@flighthq/types/contract';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { arePermissionGuardsEnabled, disablePermissionGuards, enablePermissionGuards } from './enablePermissionGuards';
|
|
6
|
+
import { requestPermission, setPermissionBackend } from './permission';
|
|
7
|
+
|
|
8
|
+
function captureLog(run: () => Promise<void>): Promise<readonly LogEntry[]> {
|
|
9
|
+
const sink = createMemoryLogSink(8);
|
|
10
|
+
addLogSink(sink.sink);
|
|
11
|
+
return run()
|
|
12
|
+
.then(() => getMemoryLogSinkEntries(sink))
|
|
13
|
+
.finally(() => removeLogSink(sink.sink));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function messageOf(entry: Readonly<LogEntry>): string {
|
|
17
|
+
const data = entry.data;
|
|
18
|
+
return typeof data === 'string' ? data : String(data.message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
disablePermissionGuards();
|
|
23
|
+
setPermissionBackend(null);
|
|
24
|
+
vi.unstubAllGlobals();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('arePermissionGuardsEnabled', () => {
|
|
28
|
+
it('reports whether diagnostics are installed', () => {
|
|
29
|
+
expect(arePermissionGuardsEnabled()).toBe(false);
|
|
30
|
+
enablePermissionGuards();
|
|
31
|
+
expect(arePermissionGuardsEnabled()).toBe(true);
|
|
32
|
+
disablePermissionGuards();
|
|
33
|
+
expect(arePermissionGuardsEnabled()).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('disablePermissionGuards', () => {
|
|
38
|
+
it('returns the silent degradation to silence', async () => {
|
|
39
|
+
vi.stubGlobal('navigator', { permissions: { query: async () => ({ state: 'prompt' }) } });
|
|
40
|
+
enablePermissionGuards();
|
|
41
|
+
disablePermissionGuards();
|
|
42
|
+
|
|
43
|
+
const entries = await captureLog(async () => {
|
|
44
|
+
await requestPermission('acme.disabled-name');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(entries.length).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('enablePermissionGuards', () => {
|
|
52
|
+
// logOnce suppresses a key for the whole PROCESS, so the fire and the stays-quiet assertions live in
|
|
53
|
+
// ONE test in order, and every test in this file uses a distinct permission name.
|
|
54
|
+
it('WARNS that a request showed no prompt, then suppresses the repeat', async () => {
|
|
55
|
+
vi.stubGlobal('navigator', { permissions: { query: async () => ({ state: 'prompt' }) } });
|
|
56
|
+
enablePermissionGuards();
|
|
57
|
+
|
|
58
|
+
const entries = await captureLog(async () => {
|
|
59
|
+
await requestPermission('acme.no-request-path');
|
|
60
|
+
});
|
|
61
|
+
expect(entries.length).toBe(1);
|
|
62
|
+
// The consequence is the point: a caller waiting on a prompt waits forever.
|
|
63
|
+
expect(messageOf(entries[0])).toContain('NO prompt was shown');
|
|
64
|
+
expect(messageOf(entries[0])).toContain('acme.no-request-path');
|
|
65
|
+
|
|
66
|
+
const repeat = await captureLog(async () => {
|
|
67
|
+
await requestPermission('acme.no-request-path');
|
|
68
|
+
});
|
|
69
|
+
expect(repeat.length).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('stays SILENT for a name that has a real request path', async () => {
|
|
73
|
+
vi.stubGlobal('navigator', { permissions: { query: async () => ({ state: 'prompt' }) } });
|
|
74
|
+
vi.stubGlobal('Notification', { permission: 'default', requestPermission: async () => 'granted' });
|
|
75
|
+
enablePermissionGuards();
|
|
76
|
+
|
|
77
|
+
const entries = await captureLog(async () => {
|
|
78
|
+
expect(await requestPermission('notifications')).toBe('granted');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(entries.length).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/permission.test.ts
CHANGED
|
@@ -3,10 +3,13 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
createWebPermissionBackend,
|
|
6
|
+
explainPermissionState,
|
|
6
7
|
getPermissionBackend,
|
|
7
8
|
getPermissionState,
|
|
9
|
+
getPermissionStates,
|
|
8
10
|
requestPermission,
|
|
9
11
|
setPermissionBackend,
|
|
12
|
+
setPermissionRequestFallbackGuard,
|
|
10
13
|
} from './permission';
|
|
11
14
|
|
|
12
15
|
// A recording mock backend: relays a fixed state and captures the last name each method saw.
|
|
@@ -149,10 +152,95 @@ describe('createWebPermissionBackend', () => {
|
|
|
149
152
|
});
|
|
150
153
|
|
|
151
154
|
it('request falls back to a state query for a name with no request path', async () => {
|
|
155
|
+
// 'push' has no concrete web trigger; midi and screen-wake-lock now do, so they are no longer
|
|
156
|
+
// examples of this branch.
|
|
152
157
|
const query = vi.fn(async () => ({ state: 'granted' }));
|
|
153
158
|
vi.stubGlobal('navigator', { permissions: { query } });
|
|
159
|
+
expect(await createWebPermissionBackend().request('push')).toBe('granted');
|
|
160
|
+
expect(query).toHaveBeenCalledWith({ name: 'push' });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('request routes midi through requestMIDIAccess, discarding the access object', async () => {
|
|
164
|
+
const requestMIDIAccess = vi.fn(async () => ({ inputs: 'discarded' }));
|
|
165
|
+
vi.stubGlobal('navigator', { requestMIDIAccess });
|
|
154
166
|
expect(await createWebPermissionBackend().request('midi')).toBe('granted');
|
|
155
|
-
expect(
|
|
167
|
+
expect(requestMIDIAccess).toHaveBeenCalledOnce();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('request reports midi denial rather than throwing', async () => {
|
|
171
|
+
vi.stubGlobal('navigator', {
|
|
172
|
+
requestMIDIAccess: async () => {
|
|
173
|
+
throw new Error('denied');
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
expect(await createWebPermissionBackend().request('midi')).toBe('denied');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('request takes a screen wake lock and RELEASES it immediately', async () => {
|
|
180
|
+
const release = vi.fn(async () => {});
|
|
181
|
+
const request = vi.fn(async () => ({ release }));
|
|
182
|
+
vi.stubGlobal('navigator', { wakeLock: { request } });
|
|
183
|
+
expect(await createWebPermissionBackend().request('screen-wake-lock')).toBe('granted');
|
|
184
|
+
expect(request).toHaveBeenCalledWith('screen');
|
|
185
|
+
// Holding the lock would be a lasting side effect the caller never asked for.
|
|
186
|
+
expect(release).toHaveBeenCalledOnce();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('request resolves to a sentinel when the wake-lock API is absent, and denied when it rejects', async () => {
|
|
190
|
+
vi.stubGlobal('navigator', {});
|
|
191
|
+
expect(await createWebPermissionBackend().request('screen-wake-lock')).toBe('prompt');
|
|
192
|
+
vi.stubGlobal('navigator', {
|
|
193
|
+
wakeLock: {
|
|
194
|
+
request: async () => {
|
|
195
|
+
throw new Error('denied');
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
expect(await createWebPermissionBackend().request('screen-wake-lock')).toBe('denied');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('explainPermissionState', () => {
|
|
204
|
+
afterEach(() => {
|
|
205
|
+
setPermissionBackend(null);
|
|
206
|
+
vi.unstubAllGlobals();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('separates the three meanings of a bare prompt: undecided, unqueryable, and unsupported', async () => {
|
|
210
|
+
// No Permissions API at all.
|
|
211
|
+
vi.stubGlobal('navigator', {});
|
|
212
|
+
expect(await explainPermissionState('camera')).toMatchObject({ source: 'unsupported', state: 'prompt' });
|
|
213
|
+
|
|
214
|
+
// Present but rejects this descriptor name.
|
|
215
|
+
vi.stubGlobal('navigator', {
|
|
216
|
+
permissions: {
|
|
217
|
+
query: () => Promise.reject(new Error('bad name')),
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
setPermissionBackend(null);
|
|
221
|
+
expect(await explainPermissionState('acme.unknown')).toMatchObject({ source: 'unqueryable' });
|
|
222
|
+
|
|
223
|
+
// Present, queryable, genuinely not decided.
|
|
224
|
+
vi.stubGlobal('navigator', { permissions: { query: () => Promise.resolve({ state: 'prompt' }) } });
|
|
225
|
+
setPermissionBackend(null);
|
|
226
|
+
expect(await explainPermissionState('camera')).toMatchObject({ source: 'undecided', state: 'prompt' });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('reports a definite answer as decided, and carries the name through', async () => {
|
|
230
|
+
vi.stubGlobal('navigator', { permissions: { query: () => Promise.resolve({ state: 'granted' }) } });
|
|
231
|
+
expect(await explainPermissionState('geolocation')).toEqual({
|
|
232
|
+
name: 'geolocation',
|
|
233
|
+
source: 'decided',
|
|
234
|
+
state: 'granted',
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('describes a custom backend without claiming branches it cannot observe', async () => {
|
|
239
|
+
// A host backend has no Permissions API to inspect, so unqueryable/unsupported are not assertable.
|
|
240
|
+
setPermissionBackend(fakeBackend('prompt'));
|
|
241
|
+
expect(await explainPermissionState('camera')).toMatchObject({ source: 'undecided' });
|
|
242
|
+
setPermissionBackend(fakeBackend('denied'));
|
|
243
|
+
expect(await explainPermissionState('camera')).toMatchObject({ source: 'decided', state: 'denied' });
|
|
156
244
|
});
|
|
157
245
|
});
|
|
158
246
|
|
|
@@ -178,6 +266,27 @@ describe('getPermissionState', () => {
|
|
|
178
266
|
});
|
|
179
267
|
});
|
|
180
268
|
|
|
269
|
+
describe('getPermissionStates', () => {
|
|
270
|
+
afterEach(() => setPermissionBackend(null));
|
|
271
|
+
|
|
272
|
+
it('resolves in INPUT order, preserving a repeated name rather than collapsing it', async () => {
|
|
273
|
+
const states: Record<string, PermissionState> = { camera: 'granted', microphone: 'denied' };
|
|
274
|
+
setPermissionBackend({
|
|
275
|
+
getState: (name: PermissionName) => Promise.resolve(states[name] ?? 'prompt'),
|
|
276
|
+
request: () => Promise.resolve('prompt' as PermissionState),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(await getPermissionStates(['microphone', 'camera', 'microphone'])).toEqual(['denied', 'granted', 'denied']);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('returns an empty array for no names, without touching the backend', async () => {
|
|
283
|
+
const backend = fakeBackend('granted');
|
|
284
|
+
setPermissionBackend(backend);
|
|
285
|
+
expect(await getPermissionStates([])).toEqual([]);
|
|
286
|
+
expect(backend.lastGetState).toBeNull();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
181
290
|
describe('requestPermission', () => {
|
|
182
291
|
it('dispatches to the active backend and relays the resolved state', async () => {
|
|
183
292
|
const backend = fakeBackend('granted');
|
|
@@ -198,3 +307,33 @@ describe('setPermissionBackend', () => {
|
|
|
198
307
|
expect(restored.getState).toBeInstanceOf(Function);
|
|
199
308
|
});
|
|
200
309
|
});
|
|
310
|
+
|
|
311
|
+
describe('setPermissionRequestFallbackGuard', () => {
|
|
312
|
+
afterEach(() => {
|
|
313
|
+
setPermissionRequestFallbackGuard(null);
|
|
314
|
+
setPermissionBackend(null);
|
|
315
|
+
vi.unstubAllGlobals();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('reports only the names that silently degraded to a query, and stops once cleared', async () => {
|
|
319
|
+
const seen: string[] = [];
|
|
320
|
+
// No request path for 'push'; 'notifications' has one via Notification.requestPermission.
|
|
321
|
+
vi.stubGlobal('navigator', { permissions: { query: () => Promise.resolve({ state: 'prompt' }) } });
|
|
322
|
+
vi.stubGlobal('Notification', {
|
|
323
|
+
permission: 'default',
|
|
324
|
+
requestPermission: () => Promise.resolve('granted'),
|
|
325
|
+
});
|
|
326
|
+
setPermissionRequestFallbackGuard((name) => seen.push(name));
|
|
327
|
+
|
|
328
|
+
expect(await requestPermission('push')).toBe('prompt');
|
|
329
|
+
expect(seen).toEqual(['push']);
|
|
330
|
+
|
|
331
|
+
// A name WITH a router prompts for real and must not be reported as a fallback.
|
|
332
|
+
expect(await requestPermission('notifications')).toBe('granted');
|
|
333
|
+
expect(seen).toEqual(['push']);
|
|
334
|
+
|
|
335
|
+
setPermissionRequestFallbackGuard(null);
|
|
336
|
+
await requestPermission('push');
|
|
337
|
+
expect(seen).toEqual(['push']);
|
|
338
|
+
});
|
|
339
|
+
});
|