@ankhorage/permissions 0.1.0

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +143 -0
  4. package/dist/client/types.d.ts +19 -0
  5. package/dist/client/types.d.ts.map +1 -0
  6. package/dist/client/types.js +2 -0
  7. package/dist/client/types.js.map +1 -0
  8. package/dist/index.d.ts +12 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +6 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/manager/createPermissionManager.d.ts +31 -0
  13. package/dist/manager/createPermissionManager.d.ts.map +1 -0
  14. package/dist/manager/createPermissionManager.js +39 -0
  15. package/dist/manager/createPermissionManager.js.map +1 -0
  16. package/dist/metadata/index.d.ts +8 -0
  17. package/dist/metadata/index.d.ts.map +1 -0
  18. package/dist/metadata/index.js +8 -0
  19. package/dist/metadata/index.js.map +1 -0
  20. package/dist/react/index.d.ts +66 -0
  21. package/dist/react/index.d.ts.map +1 -0
  22. package/dist/react/index.js +91 -0
  23. package/dist/react/index.js.map +1 -0
  24. package/dist/registry/permissions.d.ts +52 -0
  25. package/dist/registry/permissions.d.ts.map +1 -0
  26. package/dist/registry/permissions.js +108 -0
  27. package/dist/registry/permissions.js.map +1 -0
  28. package/dist/state/permissionState.d.ts +43 -0
  29. package/dist/state/permissionState.d.ts.map +1 -0
  30. package/dist/state/permissionState.js +44 -0
  31. package/dist/state/permissionState.js.map +1 -0
  32. package/dist/testing/index.d.ts +49 -0
  33. package/dist/testing/index.d.ts.map +1 -0
  34. package/dist/testing/index.js +95 -0
  35. package/dist/testing/index.js.map +1 -0
  36. package/dist/web/index.d.ts +60 -0
  37. package/dist/web/index.d.ts.map +1 -0
  38. package/dist/web/index.js +234 -0
  39. package/dist/web/index.js.map +1 -0
  40. package/package.json +114 -0
  41. package/src/client/types.ts +19 -0
  42. package/src/index.ts +31 -0
  43. package/src/manager/createPermissionManager.test.ts +67 -0
  44. package/src/manager/createPermissionManager.ts +54 -0
  45. package/src/metadata/index.ts +7 -0
  46. package/src/react/index.test.ts +11 -0
  47. package/src/react/index.tsx +170 -0
  48. package/src/registry/permissions.test.ts +37 -0
  49. package/src/registry/permissions.ts +128 -0
  50. package/src/state/permissionState.test.ts +54 -0
  51. package/src/state/permissionState.ts +86 -0
  52. package/src/testing/index.test.ts +57 -0
  53. package/src/testing/index.ts +168 -0
  54. package/src/web/index.test.ts +205 -0
  55. package/src/web/index.ts +390 -0
@@ -0,0 +1,234 @@
1
+ import { assertKnownPermission, Permission } from '../registry/permissions';
2
+ import { createPermissionState, } from '../state/permissionState';
3
+ /***
4
+ * Creates a browser permission client using guarded structural globals.
5
+ *
6
+ * @readme
7
+ * Unsupported web APIs resolve to `status: 'unavailable'`. The adapter does not
8
+ * import DOM types and does not assume it is running in a browser.
9
+ */
10
+ export function createWebPermissionClient(options = {}) {
11
+ const global = options.global ?? getDefaultGlobal();
12
+ return {
13
+ async getStatus(permission) {
14
+ const knownPermission = assertKnownPermission(permission);
15
+ switch (knownPermission) {
16
+ case Permission.Notifications:
17
+ return getNotificationStatus(knownPermission, global);
18
+ case Permission.LocationForeground:
19
+ return getPermissionApiStatus(knownPermission, global, 'geolocation', 'Geolocation is unavailable in this environment.');
20
+ case Permission.Camera:
21
+ return getMediaStatus(knownPermission, global, 'camera');
22
+ case Permission.Microphone:
23
+ return getMediaStatus(knownPermission, global, 'microphone');
24
+ default:
25
+ return unavailable(knownPermission, 'This permission is not supported by the web adapter.');
26
+ }
27
+ },
28
+ async request(permission) {
29
+ const knownPermission = assertKnownPermission(permission);
30
+ switch (knownPermission) {
31
+ case Permission.Notifications:
32
+ return requestNotifications(knownPermission, global);
33
+ case Permission.LocationForeground:
34
+ return requestGeolocation(knownPermission, global);
35
+ case Permission.Camera:
36
+ return requestMedia(knownPermission, global, { video: true });
37
+ case Permission.Microphone:
38
+ return requestMedia(knownPermission, global, { audio: true });
39
+ default:
40
+ return unavailable(knownPermission, 'This permission is not supported by the web adapter.');
41
+ }
42
+ },
43
+ };
44
+ }
45
+ function getDefaultGlobal() {
46
+ return globalThis;
47
+ }
48
+ function getNotificationStatus(permission, global) {
49
+ if (global.Notification === undefined) {
50
+ return unavailable(permission, 'Notification API is unavailable.');
51
+ }
52
+ return stateFromNotificationPermission(permission, global.Notification.permission);
53
+ }
54
+ async function requestNotifications(permission, global) {
55
+ if (global.Notification === undefined) {
56
+ return unavailable(permission, 'Notification API is unavailable.');
57
+ }
58
+ try {
59
+ const result = await global.Notification.requestPermission();
60
+ return stateFromNotificationPermission(permission, result);
61
+ }
62
+ catch (error) {
63
+ return unavailable(permission, `Notification permission request failed${formatErrorName(error)}.`);
64
+ }
65
+ }
66
+ async function getPermissionApiStatus(permission, global, name, unavailableReason) {
67
+ if (global.navigator?.permissions === undefined) {
68
+ return unavailable(permission, unavailableReason);
69
+ }
70
+ try {
71
+ const status = await global.navigator.permissions.query({ name });
72
+ return stateFromBrowserPermission(permission, status.state);
73
+ }
74
+ catch {
75
+ return unavailable(permission, unavailableReason);
76
+ }
77
+ }
78
+ async function getMediaStatus(permission, global, name) {
79
+ if (global.navigator?.permissions !== undefined) {
80
+ try {
81
+ const status = await global.navigator.permissions.query({ name });
82
+ return stateFromBrowserPermission(permission, status.state);
83
+ }
84
+ catch {
85
+ return getMediaAvailability(permission, global);
86
+ }
87
+ }
88
+ return getMediaAvailability(permission, global);
89
+ }
90
+ function getMediaAvailability(permission, global) {
91
+ if (global.isSecureContext === false) {
92
+ return createPermissionState({
93
+ permission,
94
+ status: 'blocked',
95
+ canAskAgain: false,
96
+ reason: 'Media permissions require a secure context.',
97
+ });
98
+ }
99
+ if (global.navigator?.mediaDevices?.getUserMedia === undefined) {
100
+ return unavailable(permission, 'Media devices API is unavailable.');
101
+ }
102
+ return createPermissionState({
103
+ permission,
104
+ status: 'unknown',
105
+ canAskAgain: true,
106
+ });
107
+ }
108
+ async function requestGeolocation(permission, global) {
109
+ if (global.isSecureContext === false) {
110
+ return createPermissionState({
111
+ permission,
112
+ status: 'blocked',
113
+ canAskAgain: false,
114
+ reason: 'Geolocation requires a secure context.',
115
+ });
116
+ }
117
+ if (global.navigator?.geolocation === undefined) {
118
+ return unavailable(permission, 'Geolocation API is unavailable.');
119
+ }
120
+ return new Promise((resolve) => {
121
+ global.navigator?.geolocation?.getCurrentPosition(() => {
122
+ resolve(createPermissionState({
123
+ permission,
124
+ status: 'granted',
125
+ canAskAgain: true,
126
+ requestedAt: new Date(),
127
+ }));
128
+ }, () => {
129
+ resolve(createPermissionState({
130
+ permission,
131
+ status: 'denied',
132
+ canAskAgain: false,
133
+ requestedAt: new Date(),
134
+ }));
135
+ }, { maximumAge: 0 });
136
+ });
137
+ }
138
+ async function requestMedia(permission, global, constraints) {
139
+ const availability = getMediaAvailability(permission, global);
140
+ if (availability.status !== 'unknown') {
141
+ return availability;
142
+ }
143
+ try {
144
+ const stream = await global.navigator?.mediaDevices?.getUserMedia(constraints);
145
+ for (const track of stream?.getTracks() ?? []) {
146
+ track.stop();
147
+ }
148
+ return createPermissionState({
149
+ permission,
150
+ status: 'granted',
151
+ canAskAgain: true,
152
+ requestedAt: new Date(),
153
+ });
154
+ }
155
+ catch (error) {
156
+ return stateFromMediaError(permission, error);
157
+ }
158
+ }
159
+ function stateFromMediaError(permission, error) {
160
+ const errorName = getErrorName(error);
161
+ const unavailableErrorNames = new Set([
162
+ 'AbortError',
163
+ 'NotFoundError',
164
+ 'NotReadableError',
165
+ 'OverconstrainedError',
166
+ 'SecurityError',
167
+ ]);
168
+ if (errorName !== undefined && unavailableErrorNames.has(errorName)) {
169
+ return createPermissionState({
170
+ permission,
171
+ status: 'unavailable',
172
+ canAskAgain: false,
173
+ requestedAt: new Date(),
174
+ reason: `Media permission request failed: ${errorName}.`,
175
+ });
176
+ }
177
+ return createPermissionState({
178
+ permission,
179
+ status: 'denied',
180
+ canAskAgain: false,
181
+ requestedAt: new Date(),
182
+ reason: errorName === undefined
183
+ ? 'Media permission request was denied.'
184
+ : `Media permission request was denied: ${errorName}.`,
185
+ });
186
+ }
187
+ function stateFromNotificationPermission(permission, status) {
188
+ if (status === 'granted') {
189
+ return createPermissionState({
190
+ permission,
191
+ status: 'granted',
192
+ canAskAgain: true,
193
+ });
194
+ }
195
+ if (status === 'denied') {
196
+ return createPermissionState({
197
+ permission,
198
+ status: 'denied',
199
+ canAskAgain: false,
200
+ });
201
+ }
202
+ return createPermissionState({
203
+ permission,
204
+ status: 'unknown',
205
+ canAskAgain: true,
206
+ });
207
+ }
208
+ function stateFromBrowserPermission(permission, status) {
209
+ const normalizedStatus = status === 'prompt' ? 'unknown' : status;
210
+ return createPermissionState({
211
+ permission,
212
+ status: normalizedStatus,
213
+ canAskAgain: status === 'prompt' || status === 'granted',
214
+ });
215
+ }
216
+ function unavailable(permission, reason) {
217
+ return createPermissionState({
218
+ permission,
219
+ status: 'unavailable',
220
+ canAskAgain: false,
221
+ reason,
222
+ });
223
+ }
224
+ function formatErrorName(error) {
225
+ const errorName = getErrorName(error);
226
+ return errorName === undefined ? '' : `: ${errorName}`;
227
+ }
228
+ function getErrorName(error) {
229
+ if (typeof error !== 'object' || error === null || !('name' in error)) {
230
+ return undefined;
231
+ }
232
+ return typeof error.name === 'string' && error.name.length > 0 ? error.name : undefined;
233
+ }
234
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PermissionClient } from '../client/types';\nimport { assertKnownPermission, Permission } from '../registry/permissions';\nimport {\n createPermissionState,\n type PermissionState,\n type PermissionStatus,\n} from '../state/permissionState';\n\ntype BrowserPermissionName = 'camera' | 'microphone' | 'geolocation' | 'notifications';\ntype BrowserPermissionState = 'granted' | 'denied' | 'prompt';\ntype NotificationPermissionState = 'default' | 'denied' | 'granted';\n\ninterface BrowserPermissionStatus {\n readonly state: BrowserPermissionState;\n}\n\ninterface BrowserPermissions {\n query(descriptor: { readonly name: BrowserPermissionName }): Promise<BrowserPermissionStatus>;\n}\n\ninterface MediaStreamTrackLike {\n stop(): void;\n}\n\ninterface MediaStreamLike {\n getTracks(): readonly MediaStreamTrackLike[];\n}\n\ninterface MediaDevicesLike {\n getUserMedia(constraints: {\n readonly audio?: boolean;\n readonly video?: boolean;\n }): Promise<MediaStreamLike>;\n}\n\ninterface GeolocationLike {\n getCurrentPosition(\n success: () => void,\n error: () => void,\n options?: {\n readonly maximumAge?: number;\n readonly timeout?: number;\n },\n ): void;\n}\n\ninterface NavigatorLike {\n readonly permissions?: BrowserPermissions;\n readonly mediaDevices?: MediaDevicesLike;\n readonly geolocation?: GeolocationLike;\n}\n\ninterface NotificationLike {\n readonly permission: NotificationPermissionState;\n requestPermission(): Promise<NotificationPermissionState> | NotificationPermissionState;\n}\n\ninterface WebGlobalLike {\n readonly isSecureContext?: boolean;\n readonly navigator?: NavigatorLike;\n readonly Notification?: NotificationLike;\n}\n\n/***\n * Options for the browser permission adapter.\n */\nexport interface WebPermissionClientOptions {\n readonly global?: WebGlobalLike;\n}\n\n/***\n * Creates a browser permission client using guarded structural globals.\n *\n * @readme\n * Unsupported web APIs resolve to `status: 'unavailable'`. The adapter does not\n * import DOM types and does not assume it is running in a browser.\n */\nexport function createWebPermissionClient(\n options: WebPermissionClientOptions = {},\n): PermissionClient {\n const global = options.global ?? getDefaultGlobal();\n\n return {\n async getStatus(permission) {\n const knownPermission = assertKnownPermission(permission);\n\n switch (knownPermission) {\n case Permission.Notifications:\n return getNotificationStatus(knownPermission, global);\n case Permission.LocationForeground:\n return getPermissionApiStatus(\n knownPermission,\n global,\n 'geolocation',\n 'Geolocation is unavailable in this environment.',\n );\n case Permission.Camera:\n return getMediaStatus(knownPermission, global, 'camera');\n case Permission.Microphone:\n return getMediaStatus(knownPermission, global, 'microphone');\n default:\n return unavailable(\n knownPermission,\n 'This permission is not supported by the web adapter.',\n );\n }\n },\n\n async request(permission) {\n const knownPermission = assertKnownPermission(permission);\n\n switch (knownPermission) {\n case Permission.Notifications:\n return requestNotifications(knownPermission, global);\n case Permission.LocationForeground:\n return requestGeolocation(knownPermission, global);\n case Permission.Camera:\n return requestMedia(knownPermission, global, { video: true });\n case Permission.Microphone:\n return requestMedia(knownPermission, global, { audio: true });\n default:\n return unavailable(\n knownPermission,\n 'This permission is not supported by the web adapter.',\n );\n }\n },\n };\n}\n\nfunction getDefaultGlobal(): WebGlobalLike {\n return globalThis as WebGlobalLike;\n}\n\nfunction getNotificationStatus(permission: Permission, global: WebGlobalLike): PermissionState {\n if (global.Notification === undefined) {\n return unavailable(permission, 'Notification API is unavailable.');\n }\n\n return stateFromNotificationPermission(permission, global.Notification.permission);\n}\n\nasync function requestNotifications(\n permission: Permission,\n global: WebGlobalLike,\n): Promise<PermissionState> {\n if (global.Notification === undefined) {\n return unavailable(permission, 'Notification API is unavailable.');\n }\n\n try {\n const result = await global.Notification.requestPermission();\n\n return stateFromNotificationPermission(permission, result);\n } catch (error) {\n return unavailable(\n permission,\n `Notification permission request failed${formatErrorName(error)}.`,\n );\n }\n}\n\nasync function getPermissionApiStatus(\n permission: Permission,\n global: WebGlobalLike,\n name: BrowserPermissionName,\n unavailableReason: string,\n): Promise<PermissionState> {\n if (global.navigator?.permissions === undefined) {\n return unavailable(permission, unavailableReason);\n }\n\n try {\n const status = await global.navigator.permissions.query({ name });\n\n return stateFromBrowserPermission(permission, status.state);\n } catch {\n return unavailable(permission, unavailableReason);\n }\n}\n\nasync function getMediaStatus(\n permission: Permission,\n global: WebGlobalLike,\n name: BrowserPermissionName,\n): Promise<PermissionState> {\n if (global.navigator?.permissions !== undefined) {\n try {\n const status = await global.navigator.permissions.query({ name });\n\n return stateFromBrowserPermission(permission, status.state);\n } catch {\n return getMediaAvailability(permission, global);\n }\n }\n\n return getMediaAvailability(permission, global);\n}\n\nfunction getMediaAvailability(permission: Permission, global: WebGlobalLike): PermissionState {\n if (global.isSecureContext === false) {\n return createPermissionState({\n permission,\n status: 'blocked',\n canAskAgain: false,\n reason: 'Media permissions require a secure context.',\n });\n }\n\n if (global.navigator?.mediaDevices?.getUserMedia === undefined) {\n return unavailable(permission, 'Media devices API is unavailable.');\n }\n\n return createPermissionState({\n permission,\n status: 'unknown',\n canAskAgain: true,\n });\n}\n\nasync function requestGeolocation(\n permission: Permission,\n global: WebGlobalLike,\n): Promise<PermissionState> {\n if (global.isSecureContext === false) {\n return createPermissionState({\n permission,\n status: 'blocked',\n canAskAgain: false,\n reason: 'Geolocation requires a secure context.',\n });\n }\n\n if (global.navigator?.geolocation === undefined) {\n return unavailable(permission, 'Geolocation API is unavailable.');\n }\n\n return new Promise((resolve) => {\n global.navigator?.geolocation?.getCurrentPosition(\n () => {\n resolve(\n createPermissionState({\n permission,\n status: 'granted',\n canAskAgain: true,\n requestedAt: new Date(),\n }),\n );\n },\n () => {\n resolve(\n createPermissionState({\n permission,\n status: 'denied',\n canAskAgain: false,\n requestedAt: new Date(),\n }),\n );\n },\n { maximumAge: 0 },\n );\n });\n}\n\nasync function requestMedia(\n permission: Permission,\n global: WebGlobalLike,\n constraints: {\n readonly audio?: boolean;\n readonly video?: boolean;\n },\n): Promise<PermissionState> {\n const availability = getMediaAvailability(permission, global);\n\n if (availability.status !== 'unknown') {\n return availability;\n }\n\n try {\n const stream = await global.navigator?.mediaDevices?.getUserMedia(constraints);\n\n for (const track of stream?.getTracks() ?? []) {\n track.stop();\n }\n\n return createPermissionState({\n permission,\n status: 'granted',\n canAskAgain: true,\n requestedAt: new Date(),\n });\n } catch (error) {\n return stateFromMediaError(permission, error);\n }\n}\n\nfunction stateFromMediaError(permission: Permission, error: unknown): PermissionState {\n const errorName = getErrorName(error);\n const unavailableErrorNames = new Set([\n 'AbortError',\n 'NotFoundError',\n 'NotReadableError',\n 'OverconstrainedError',\n 'SecurityError',\n ]);\n\n if (errorName !== undefined && unavailableErrorNames.has(errorName)) {\n return createPermissionState({\n permission,\n status: 'unavailable',\n canAskAgain: false,\n requestedAt: new Date(),\n reason: `Media permission request failed: ${errorName}.`,\n });\n }\n\n return createPermissionState({\n permission,\n status: 'denied',\n canAskAgain: false,\n requestedAt: new Date(),\n reason:\n errorName === undefined\n ? 'Media permission request was denied.'\n : `Media permission request was denied: ${errorName}.`,\n });\n}\n\nfunction stateFromNotificationPermission(\n permission: Permission,\n status: NotificationPermissionState,\n): PermissionState {\n if (status === 'granted') {\n return createPermissionState({\n permission,\n status: 'granted',\n canAskAgain: true,\n });\n }\n\n if (status === 'denied') {\n return createPermissionState({\n permission,\n status: 'denied',\n canAskAgain: false,\n });\n }\n\n return createPermissionState({\n permission,\n status: 'unknown',\n canAskAgain: true,\n });\n}\n\nfunction stateFromBrowserPermission(\n permission: Permission,\n status: BrowserPermissionState,\n): PermissionState {\n const normalizedStatus: PermissionStatus = status === 'prompt' ? 'unknown' : status;\n\n return createPermissionState({\n permission,\n status: normalizedStatus,\n canAskAgain: status === 'prompt' || status === 'granted',\n });\n}\n\nfunction unavailable(permission: Permission, reason: string): PermissionState {\n return createPermissionState({\n permission,\n status: 'unavailable',\n canAskAgain: false,\n reason,\n });\n}\n\nfunction formatErrorName(error: unknown): string {\n const errorName = getErrorName(error);\n\n return errorName === undefined ? '' : `: ${errorName}`;\n}\n\nfunction getErrorName(error: unknown): string | undefined {\n if (typeof error !== 'object' || error === null || !('name' in error)) {\n return undefined;\n }\n\n return typeof error.name === 'string' && error.name.length > 0 ? error.name : undefined;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "@ankhorage/permissions",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "Cross-platform permission registry and runtime helpers for TypeScript apps, with unified request/check APIs for camera, media, location, notifications, microphone, etc. and web/native capabilities.",
6
+ "homepage": "https://github.com/ankhorage/permissions#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/ankhorage/permissions/issues"
9
+ },
10
+ "license": "MIT",
11
+ "main": "./dist/index.js",
12
+ "sideEffects": false,
13
+ "types": "./dist/index.d.ts",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/ankhorage/permissions.git"
17
+ },
18
+ "keywords": [
19
+ "permissions",
20
+ "permission-management",
21
+ "capabilities",
22
+ "typescript",
23
+ "react",
24
+ "react-native",
25
+ "react-native-web",
26
+ "expo",
27
+ "web",
28
+ "camera",
29
+ "notifications",
30
+ "location",
31
+ "media-library",
32
+ "microphone",
33
+ "privacy",
34
+ "cross-platform"
35
+ ],
36
+ "dependencies": {},
37
+ "peerDependencies": {
38
+ "react": ">=18.2.0 || >=19.0.0"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "src",
43
+ "package.json",
44
+ "README.md",
45
+ "CHANGELOG.md",
46
+ "LICENSE"
47
+ ],
48
+ "exports": {
49
+ ".": {
50
+ "react-native": "./dist/index.js",
51
+ "browser": "./dist/index.js",
52
+ "types": "./dist/index.d.ts",
53
+ "import": "./dist/index.js",
54
+ "default": "./dist/index.js"
55
+ },
56
+ "./react": {
57
+ "react-native": "./dist/react/index.js",
58
+ "browser": "./dist/react/index.js",
59
+ "types": "./dist/react/index.d.ts",
60
+ "import": "./dist/react/index.js",
61
+ "default": "./dist/react/index.js"
62
+ },
63
+ "./testing": {
64
+ "react-native": "./dist/testing/index.js",
65
+ "browser": "./dist/testing/index.js",
66
+ "types": "./dist/testing/index.d.ts",
67
+ "import": "./dist/testing/index.js",
68
+ "default": "./dist/testing/index.js"
69
+ },
70
+ "./web": {
71
+ "browser": "./dist/web/index.js",
72
+ "types": "./dist/web/index.d.ts",
73
+ "import": "./dist/web/index.js",
74
+ "default": "./dist/web/index.js"
75
+ },
76
+ "./metadata": {
77
+ "react-native": "./dist/metadata/index.js",
78
+ "browser": "./dist/metadata/index.js",
79
+ "types": "./dist/metadata/index.d.ts",
80
+ "import": "./dist/metadata/index.js",
81
+ "default": "./dist/metadata/index.js"
82
+ },
83
+ "./package.json": "./package.json"
84
+ },
85
+ "publishConfig": {
86
+ "access": "public"
87
+ },
88
+ "scripts": {
89
+ "build": "rm -rf dist tsconfig.tsbuildinfo && bun x tsc -p tsconfig.json",
90
+ "changeset": "changeset",
91
+ "changeset:status": "changeset status --since=origin/main",
92
+ "docs": "paradox",
93
+ "knip": "ankhorage-knip",
94
+ "lint": "ankhorage-eslint . --max-warnings=0",
95
+ "lint:fix": "ankhorage-eslint . --fix --max-warnings=0",
96
+ "format": "ankhorage-prettier --write .",
97
+ "format:check": "ankhorage-prettier --check .",
98
+ "prepack": "bun run build",
99
+ "test": "bun test src",
100
+ "typecheck": "bun x tsc --noEmit -p tsconfig.json",
101
+ "version-packages": "changeset version"
102
+ },
103
+ "devDependencies": {
104
+ "@ankhorage/devtools": "^1.0.6",
105
+ "@ankhorage/paradox": "^0.1.10",
106
+ "@changesets/cli": "^2.31.0",
107
+ "@types/bun": "^1.3.13",
108
+ "@types/node": "^25.6.0",
109
+ "@types/react": "^19.2.7",
110
+ "react": "^19.2.1",
111
+ "typescript": "^5.9.3"
112
+ },
113
+ "packageManager": "bun@1.3.13"
114
+ }
@@ -0,0 +1,19 @@
1
+ import type { Permission } from '../registry/permissions';
2
+ import type { PermissionState } from '../state/permissionState';
3
+
4
+ /***
5
+ * Adapter-neutral interface for runtime permission checks and requests.
6
+ *
7
+ * @usage
8
+ * ```ts
9
+ * const state = await client.getStatus(Permission.Camera);
10
+ * if (!state.granted) {
11
+ * await client.request(Permission.Camera);
12
+ * }
13
+ * ```
14
+ */
15
+ export interface PermissionClient {
16
+ getStatus(permission: Permission): Promise<PermissionState>;
17
+ request(permission: Permission): Promise<PermissionState>;
18
+ openSettings?(): Promise<void>;
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ export type { PermissionClient } from './client/types';
2
+ export type { PermissionManager } from './manager/createPermissionManager';
3
+ export { createPermissionManager } from './manager/createPermissionManager';
4
+ export type {
5
+ PermissionHookResult,
6
+ PermissionsProviderProps,
7
+ UsePermissionOptions,
8
+ } from './react/index';
9
+ export { PermissionsProvider, usePermission, usePermissions } from './react/index';
10
+ export type { PermissionDefinition, PermissionEnvironment } from './registry/permissions';
11
+ export {
12
+ assertKnownPermission,
13
+ getPermissionDefinition,
14
+ isPermission,
15
+ Permission,
16
+ PERMISSION_REGISTRY,
17
+ PERMISSIONS,
18
+ } from './registry/permissions';
19
+ export type { PermissionState, PermissionStatus } from './state/permissionState';
20
+ export {
21
+ createPermissionState,
22
+ isPermissionStatus,
23
+ normalizePermissionState,
24
+ PERMISSION_STATUSES,
25
+ } from './state/permissionState';
26
+ export type {
27
+ FakePermissionClient,
28
+ FakePermissionClientOptions,
29
+ FakePermissionStateSeed,
30
+ } from './testing/index';
31
+ export { createFakePermissionClient } from './testing/index';
@@ -0,0 +1,67 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { Permission } from '../registry/permissions';
4
+ import { createFakePermissionClient } from '../testing/index';
5
+ import { createPermissionManager } from './createPermissionManager';
6
+
7
+ describe('createPermissionManager', () => {
8
+ test('normalizes getStatus results', async () => {
9
+ const manager = createPermissionManager(
10
+ createFakePermissionClient({
11
+ initialStates: [
12
+ {
13
+ permission: Permission.Camera,
14
+ status: 'granted',
15
+ },
16
+ ],
17
+ }),
18
+ );
19
+
20
+ const state = await manager.getStatus(Permission.Camera);
21
+
22
+ expect(state).toMatchObject({
23
+ permission: Permission.Camera,
24
+ status: 'granted',
25
+ granted: true,
26
+ });
27
+ });
28
+
29
+ test('does not throw for normal denial', async () => {
30
+ const manager = createPermissionManager(
31
+ createFakePermissionClient({
32
+ requestStates: [
33
+ {
34
+ permission: Permission.Notifications,
35
+ status: 'denied',
36
+ canAskAgain: false,
37
+ },
38
+ ],
39
+ }),
40
+ );
41
+
42
+ const state = await manager.request(Permission.Notifications);
43
+
44
+ expect(state).toMatchObject({
45
+ status: 'denied',
46
+ granted: false,
47
+ canAskAgain: false,
48
+ });
49
+ });
50
+
51
+ test('exposes openSettings only when the client supports it', async () => {
52
+ let opened = false;
53
+ const manager = createPermissionManager(
54
+ createFakePermissionClient({
55
+ openSettings: () => {
56
+ opened = true;
57
+
58
+ return Promise.resolve();
59
+ },
60
+ }),
61
+ );
62
+
63
+ await manager.openSettings?.();
64
+
65
+ expect(opened).toBe(true);
66
+ });
67
+ });
@@ -0,0 +1,54 @@
1
+ import type { PermissionClient } from '../client/types';
2
+ import { assertKnownPermission, type Permission } from '../registry/permissions';
3
+ import { normalizePermissionState, type PermissionState } from '../state/permissionState';
4
+
5
+ /***
6
+ * Public manager returned by `createPermissionManager`.
7
+ */
8
+ export interface PermissionManager {
9
+ getStatus(permission: Permission): Promise<PermissionState>;
10
+ request(permission: Permission): Promise<PermissionState>;
11
+ openSettings?(): Promise<void>;
12
+ }
13
+
14
+ /***
15
+ * Wraps a client with registry validation and normalized results.
16
+ *
17
+ * @readme
18
+ * Runtime permissions and build-time native configuration are separate concerns.
19
+ * This manager checks or requests permissions through a client, but it does not
20
+ * generate iOS usage descriptions, Android manifest entries, or Expo config
21
+ * plugins.
22
+ *
23
+ * @usage
24
+ * ```ts
25
+ * const permissions = createPermissionManager(client);
26
+ * const camera = await permissions.getStatus(Permission.Camera);
27
+ * const requested = camera.granted
28
+ * ? camera
29
+ * : await permissions.request(Permission.Camera);
30
+ * ```
31
+ */
32
+ export function createPermissionManager(client: PermissionClient): PermissionManager {
33
+ const manager: PermissionManager = {
34
+ async getStatus(permission) {
35
+ const knownPermission = assertKnownPermission(permission);
36
+ const state = await client.getStatus(knownPermission);
37
+
38
+ return normalizePermissionState(knownPermission, state);
39
+ },
40
+
41
+ async request(permission) {
42
+ const knownPermission = assertKnownPermission(permission);
43
+ const state = await client.request(knownPermission);
44
+
45
+ return normalizePermissionState(knownPermission, state);
46
+ },
47
+ };
48
+
49
+ if (client.openSettings !== undefined) {
50
+ manager.openSettings = () => client.openSettings?.() ?? Promise.resolve();
51
+ }
52
+
53
+ return manager;
54
+ }
@@ -0,0 +1,7 @@
1
+ /***
2
+ * Public package metadata used by generated documentation and tooling.
3
+ */
4
+ export const permissionsPackageMetadata = {
5
+ name: '@ankhorage/permissions',
6
+ description: 'Cross-platform permission registry and runtime helpers for TypeScript apps.',
7
+ } as const;
@@ -0,0 +1,11 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { PermissionsProvider, usePermission, usePermissions } from './index';
4
+
5
+ describe('react exports', () => {
6
+ test('exposes provider and hooks without rendering dependencies', () => {
7
+ expect(typeof PermissionsProvider).toBe('function');
8
+ expect(typeof usePermissions).toBe('function');
9
+ expect(typeof usePermission).toBe('function');
10
+ });
11
+ });