@ccheever/exact-ibex-runtime 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.
- package/package.json +63 -0
- package/src/abort/AbortController.ts +23 -0
- package/src/abort/AbortSignal.ts +152 -0
- package/src/abort/index.ts +2 -0
- package/src/accessibility.ts +12 -0
- package/src/arraybuffer-detach.ts +109 -0
- package/src/base64/base64.ts +168 -0
- package/src/base64/index.ts +1 -0
- package/src/blob/Blob.ts +259 -0
- package/src/blob/File.ts +59 -0
- package/src/blob/FormData.ts +323 -0
- package/src/blob/index.ts +3 -0
- package/src/bootstrap.ts +1946 -0
- package/src/broadcast/BroadcastChannel.ts +280 -0
- package/src/broadcast/index.ts +5 -0
- package/src/cache/Cache.ts +349 -0
- package/src/cache/CacheStorage.ts +89 -0
- package/src/cache/index.ts +27 -0
- package/src/camera/index.ts +6202 -0
- package/src/camera/processor.worker.ts +194 -0
- package/src/camera/scene.ts +195 -0
- package/src/clipboard/Clipboard.ts +129 -0
- package/src/clipboard/ClipboardItem.ts +97 -0
- package/src/clipboard/index.ts +6 -0
- package/src/clone/index.ts +1 -0
- package/src/clone/structuredClone.ts +389 -0
- package/src/clone/transferableSymbols.ts +2 -0
- package/src/compression/CompressionStream.ts +146 -0
- package/src/compression/DecompressionStream.ts +342 -0
- package/src/compression/index.ts +4 -0
- package/src/console/Console.ts +341 -0
- package/src/console/index.ts +2 -0
- package/src/core/accessibility-state.ts +263 -0
- package/src/core/accessibility.ts +184 -0
- package/src/core/agent-state.ts +37 -0
- package/src/core/diagnostics-logs.ts +144 -0
- package/src/core/host-call-bridge.ts +16 -0
- package/src/core/i18n-helpers.ts +189 -0
- package/src/core/locale-state.ts +253 -0
- package/src/core/locale.ts +95 -0
- package/src/crypto/Crypto.ts +2743 -0
- package/src/crypto/index.ts +1 -0
- package/src/diagnostics/logs.ts +7 -0
- package/src/encoding/TextDecoder.ts +1181 -0
- package/src/encoding/TextDecoderStream.ts +58 -0
- package/src/encoding/TextEncoder.ts +180 -0
- package/src/encoding/TextEncoderStream.ts +39 -0
- package/src/encoding/index.ts +8 -0
- package/src/events/CloseEvent.ts +91 -0
- package/src/events/DOMException.ts +409 -0
- package/src/events/ErrorEvent.ts +39 -0
- package/src/events/Event.ts +151 -0
- package/src/events/EventTarget.ts +280 -0
- package/src/events/FocusEvent.ts +27 -0
- package/src/events/KeyboardEvent.ts +46 -0
- package/src/events/MessageEvent.ts +61 -0
- package/src/events/ProgressEvent.ts +33 -0
- package/src/events/PromiseRejectionEvent.ts +31 -0
- package/src/events/index.ts +52 -0
- package/src/eventsource/EventSource.ts +371 -0
- package/src/eventsource/index.ts +2 -0
- package/src/fetch/Headers.ts +642 -0
- package/src/fetch/Request.ts +760 -0
- package/src/fetch/Response.ts +543 -0
- package/src/fetch/body.ts +1256 -0
- package/src/fetch/cookie-jar.ts +566 -0
- package/src/fetch/demo.ts +207 -0
- package/src/fetch/errors.ts +101 -0
- package/src/fetch/fetch.ts +2610 -0
- package/src/fetch/index.ts +101 -0
- package/src/fetch/native-bridge.ts +65 -0
- package/src/fetch/types.ts +258 -0
- package/src/filereader/FileReader.ts +236 -0
- package/src/filereader/index.ts +1 -0
- package/src/fs/Dirent.ts +39 -0
- package/src/fs/ExactFile.ts +450 -0
- package/src/fs/Stats.ts +80 -0
- package/src/fs/index.ts +944 -0
- package/src/fs/promises.ts +386 -0
- package/src/fs/shared.ts +328 -0
- package/src/http-server/index.js +697 -0
- package/src/http-server/index.ts +27 -0
- package/src/identity.generated.ts +14 -0
- package/src/index.ts +283 -0
- package/src/indexeddb/IDBCursor.ts +188 -0
- package/src/indexeddb/IDBDatabase.ts +343 -0
- package/src/indexeddb/IDBFactory.ts +269 -0
- package/src/indexeddb/IDBIndex.ts +194 -0
- package/src/indexeddb/IDBKeyRange.ts +109 -0
- package/src/indexeddb/IDBObjectStore.ts +468 -0
- package/src/indexeddb/IDBRequest.ts +163 -0
- package/src/indexeddb/IDBTransaction.ts +207 -0
- package/src/indexeddb/index.ts +34 -0
- package/src/indexeddb/utils.ts +52 -0
- package/src/inspect/index.ts +1 -0
- package/src/inspect/inspect.ts +465 -0
- package/src/internal/detect.ts +104 -0
- package/src/locale.ts +10 -0
- package/src/location/index.ts +1059 -0
- package/src/locks/LockManager.ts +460 -0
- package/src/locks/index.ts +12 -0
- package/src/media/VideoFrame.ts +58 -0
- package/src/messaging/MessageChannel.ts +31 -0
- package/src/messaging/MessagePort.ts +180 -0
- package/src/messaging/index.ts +2 -0
- package/src/messaging.ts +247 -0
- package/src/native/NativeModules.ts +354 -0
- package/src/native/index.ts +1 -0
- package/src/navigator/Navigator.ts +351 -0
- package/src/navigator/index.ts +1 -0
- package/src/node/Buffer.ts +1786 -0
- package/src/node/index.ts +4 -0
- package/src/node/path.ts +495 -0
- package/src/node/process.ts +2528 -0
- package/src/performance/Performance.ts +532 -0
- package/src/performance/index.ts +21 -0
- package/src/polyfills/array.ts +236 -0
- package/src/polyfills/arraybuffer.ts +172 -0
- package/src/polyfills/groupby.ts +85 -0
- package/src/polyfills/index.ts +85 -0
- package/src/polyfills/intl.ts +1956 -0
- package/src/polyfills/iterator.ts +479 -0
- package/src/polyfills/promise.ts +37 -0
- package/src/polyfills/set.ts +245 -0
- package/src/polyfills/string.ts +85 -0
- package/src/polyfills/typedarray.ts +110 -0
- package/src/promise-rejection-tracking.ts +464 -0
- package/src/react-native/index.ts +388 -0
- package/src/runtime-entry.ts +55 -0
- package/src/scheduling/AnimationFrame.ts +105 -0
- package/src/scheduling/IdleCallback.ts +167 -0
- package/src/scheduling/index.ts +13 -0
- package/src/security/Capabilities.ts +1146 -0
- package/src/security/Permissions.ts +392 -0
- package/src/security/capability-bits.generated.ts +63 -0
- package/src/security/index.ts +16 -0
- package/src/sqlite/Database.ts +456 -0
- package/src/sqlite/Statement.ts +206 -0
- package/src/sqlite/constants.ts +79 -0
- package/src/sqlite/errors.ts +25 -0
- package/src/sqlite/index.ts +34 -0
- package/src/sqlite/module.js +438 -0
- package/src/storage/Storage.ts +291 -0
- package/src/storage/StorageManager.ts +91 -0
- package/src/storage/index.ts +3 -0
- package/src/stream-compat.ts +47 -0
- package/src/streams/ReadableStream.ts +4131 -0
- package/src/streams/TransformStream.ts +375 -0
- package/src/streams/WritableStream.ts +866 -0
- package/src/streams/index.ts +41 -0
- package/src/timers/Timers.ts +296 -0
- package/src/timers/index.ts +11 -0
- package/src/url/URL.ts +656 -0
- package/src/url/URLPattern.ts +850 -0
- package/src/url/URLSearchParams.ts +244 -0
- package/src/url/index.ts +9 -0
- package/src/websocket/WebSocket.ts +770 -0
- package/src/websocket/WebSocketError.ts +52 -0
- package/src/websocket/WebSocketStream.ts +628 -0
- package/src/websocket/index.ts +7 -0
- package/src/window/index.ts +872 -0
|
@@ -0,0 +1,1146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability Security System for Ibex Runtime
|
|
3
|
+
*
|
|
4
|
+
* Implements the capability-based security model as defined in JS_RUNTIME_SECURITY.md.
|
|
5
|
+
*
|
|
6
|
+
* The effective permission for any privileged action is the intersection of:
|
|
7
|
+
* 1. OS Permission (root) - enforced at native layer
|
|
8
|
+
* 2. App Root Capabilities - set when runtime is created
|
|
9
|
+
* 3. View Broker Grant - per-view user consent
|
|
10
|
+
* 4. Module Import Capabilities - per-module grants
|
|
11
|
+
*
|
|
12
|
+
* @see JS_RUNTIME_SECURITY.md
|
|
13
|
+
* @see JS_CAPABILITY_SECURITY_MODEL_SPEC_AND_PLAN.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
CapabilityDeniedError,
|
|
18
|
+
createCapabilityDeniedError,
|
|
19
|
+
type CapabilityDenialReason,
|
|
20
|
+
} from '../events/DOMException';
|
|
21
|
+
import { CapabilityBit } from './capability-bits.generated';
|
|
22
|
+
|
|
23
|
+
export { CapabilityBit } from './capability-bits.generated';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Capability Types
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Capability categories aligned with JS_RUNTIME_SECURITY.md Section 5
|
|
31
|
+
*/
|
|
32
|
+
export type CapabilityCategory =
|
|
33
|
+
| 'fs'
|
|
34
|
+
| 'network'
|
|
35
|
+
| 'env'
|
|
36
|
+
| 'process'
|
|
37
|
+
| 'ipc'
|
|
38
|
+
| 'crypto'
|
|
39
|
+
| 'time'
|
|
40
|
+
| 'device'
|
|
41
|
+
| 'storage'
|
|
42
|
+
| 'clipboard'
|
|
43
|
+
| 'sqlite';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Full capability specification with optional parameters.
|
|
47
|
+
* Examples:
|
|
48
|
+
* - "network:fetch" - general fetch capability
|
|
49
|
+
* - "network:fetch:api.example.com" - fetch to specific host
|
|
50
|
+
* - "fs:read:/data" - read from specific path
|
|
51
|
+
* - "device:location" - device location access
|
|
52
|
+
*/
|
|
53
|
+
export type Capability = string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Denial reasons aligned with JS_CAPABILITY_SECURITY_MODEL_SPEC_AND_PLAN.md
|
|
57
|
+
*
|
|
58
|
+
* Note: This is intentionally a string type (not a union) to allow for
|
|
59
|
+
* forward compatibility with new denial reasons from future OS versions.
|
|
60
|
+
* Use DenialReasonCategory for switch statements to handle unknown reasons.
|
|
61
|
+
*/
|
|
62
|
+
export type DenialReason =
|
|
63
|
+
| 'os_denied' // User denied at OS level
|
|
64
|
+
| 'os_restricted' // System policy (parental controls, MDM)
|
|
65
|
+
| 'broker_denied' // View broker denied
|
|
66
|
+
| 'module_not_granted' // Import capability missing
|
|
67
|
+
| 'app_not_granted' // App root capability missing
|
|
68
|
+
| 'requires_settings' // User must enable in Settings
|
|
69
|
+
| 'capability_unknown' // Capability not recognized by runtime
|
|
70
|
+
| 'os_version_too_low' // OS version doesn't support this capability
|
|
71
|
+
| 'temporarily_unavailable' // Capability temporarily unavailable (e.g., airplane mode)
|
|
72
|
+
| (string & {}); // Allow unknown reasons for forward compatibility
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Broad categories for denial reasons.
|
|
76
|
+
* Use this for switch statements to handle unknown specific reasons gracefully.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const category = getDenialReasonCategory(error.denialReason);
|
|
81
|
+
* switch (category) {
|
|
82
|
+
* case 'user_action_required':
|
|
83
|
+
* // Show UI to guide user to settings
|
|
84
|
+
* break;
|
|
85
|
+
* case 'permanently_unavailable':
|
|
86
|
+
* // Hide the feature entirely
|
|
87
|
+
* break;
|
|
88
|
+
* case 'temporarily_unavailable':
|
|
89
|
+
* // Show retry option
|
|
90
|
+
* break;
|
|
91
|
+
* default:
|
|
92
|
+
* // Generic "permission denied" message
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export type DenialReasonCategory =
|
|
97
|
+
| 'user_action_required' // User can fix by granting permission
|
|
98
|
+
| 'system_restricted' // System policy prevents access
|
|
99
|
+
| 'not_configured' // App/module hasn't been granted capability
|
|
100
|
+
| 'temporarily_unavailable' // May work later (network issues, etc.)
|
|
101
|
+
| 'permanently_unavailable' // Will never work (OS too old, etc.)
|
|
102
|
+
| 'unknown'; // Unrecognized reason
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Map a denial reason to its broad category for forward-compatible handling.
|
|
106
|
+
*/
|
|
107
|
+
export function getDenialReasonCategory(reason: DenialReason): DenialReasonCategory {
|
|
108
|
+
switch (reason) {
|
|
109
|
+
case 'os_denied':
|
|
110
|
+
case 'requires_settings':
|
|
111
|
+
return 'user_action_required';
|
|
112
|
+
|
|
113
|
+
case 'os_restricted':
|
|
114
|
+
return 'system_restricted';
|
|
115
|
+
|
|
116
|
+
case 'broker_denied':
|
|
117
|
+
case 'module_not_granted':
|
|
118
|
+
case 'app_not_granted':
|
|
119
|
+
return 'not_configured';
|
|
120
|
+
|
|
121
|
+
case 'temporarily_unavailable':
|
|
122
|
+
return 'temporarily_unavailable';
|
|
123
|
+
|
|
124
|
+
case 'capability_unknown':
|
|
125
|
+
case 'os_version_too_low':
|
|
126
|
+
return 'permanently_unavailable';
|
|
127
|
+
|
|
128
|
+
default:
|
|
129
|
+
// Forward compatibility: unknown reasons default to 'unknown'
|
|
130
|
+
// This allows new OS-specific reasons to be handled gracefully
|
|
131
|
+
return 'unknown';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Capability check result with reason for denials
|
|
137
|
+
*/
|
|
138
|
+
export interface CapabilityCheckResult {
|
|
139
|
+
allowed: boolean;
|
|
140
|
+
reason?: DenialReason;
|
|
141
|
+
capability: Capability;
|
|
142
|
+
/** Whether the capability can be requested again */
|
|
143
|
+
canRequestAgain?: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Audit log event for capability checks
|
|
148
|
+
*/
|
|
149
|
+
export interface CapabilityAuditEvent {
|
|
150
|
+
event: 'capability_granted' | 'capability_denied';
|
|
151
|
+
/** The action being performed */
|
|
152
|
+
action: 'check' | 'request' | 'use';
|
|
153
|
+
capability: Capability;
|
|
154
|
+
moduleId?: number;
|
|
155
|
+
source: 'os' | 'broker' | 'module' | 'app';
|
|
156
|
+
reason?: string;
|
|
157
|
+
timestamp: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// Native Capability Module Interface
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Interface for the native capability checking module.
|
|
166
|
+
* The native layer is the authoritative source for capability enforcement.
|
|
167
|
+
*/
|
|
168
|
+
export interface NativeCapabilityModule {
|
|
169
|
+
/**
|
|
170
|
+
* Check if a capability is granted.
|
|
171
|
+
* This should verify all layers: OS, app root, broker, and module.
|
|
172
|
+
*/
|
|
173
|
+
checkCapability(capability: Capability, moduleId?: number): boolean;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Request a capability from the broker (may trigger user prompt).
|
|
177
|
+
* Returns true if granted, false if denied.
|
|
178
|
+
*/
|
|
179
|
+
requestCapability(capability: Capability): Promise<boolean>;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get the current capability grants for the view.
|
|
183
|
+
*/
|
|
184
|
+
getGrantedCapabilities(): Capability[];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Register an audit callback for capability events.
|
|
188
|
+
*/
|
|
189
|
+
onAuditEvent?(callback: (event: CapabilityAuditEvent) => void): void;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// Capability Registry
|
|
194
|
+
// ============================================================================
|
|
195
|
+
|
|
196
|
+
let _nativeCapabilityModule: NativeCapabilityModule | null = null;
|
|
197
|
+
let _auditCallbacks: Array<(event: CapabilityAuditEvent) => void> = [];
|
|
198
|
+
|
|
199
|
+
// In-memory fallback for testing (grants everything)
|
|
200
|
+
let _testMode = false;
|
|
201
|
+
let _testGrants = new Set<Capability>();
|
|
202
|
+
let _silentMode = false;
|
|
203
|
+
|
|
204
|
+
// Strict mode - when enabled, denies capabilities without native module
|
|
205
|
+
// Default is lax mode (allows all) to avoid friction during development
|
|
206
|
+
let _strictMode = false;
|
|
207
|
+
|
|
208
|
+
// Track capabilities we've warned about to reduce console noise
|
|
209
|
+
const _warnedCapabilities = new Set<Capability>();
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if we should suppress security warnings (for tests).
|
|
213
|
+
*/
|
|
214
|
+
export function isSilentMode(): boolean {
|
|
215
|
+
return _silentMode;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check if strict capability enforcement is enabled.
|
|
220
|
+
*/
|
|
221
|
+
export function isStrictMode(): boolean {
|
|
222
|
+
return _strictMode;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Enable strict capability enforcement.
|
|
227
|
+
*
|
|
228
|
+
* When strict mode is enabled, capability checks will DENY requests
|
|
229
|
+
* when no native capability module is available. This is the secure
|
|
230
|
+
* behavior for production apps.
|
|
231
|
+
*
|
|
232
|
+
* By default, the capability system is LAX - it allows all capabilities
|
|
233
|
+
* when no native module is present, to reduce friction during development.
|
|
234
|
+
*
|
|
235
|
+
* Call this during app initialization for production builds:
|
|
236
|
+
* ```ts
|
|
237
|
+
* import { enableStrictMode } from '@exact/runtime/security';
|
|
238
|
+
*
|
|
239
|
+
* if (__PROD__) {
|
|
240
|
+
* enableStrictMode();
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*
|
|
244
|
+
* The native layer should call this automatically for release builds.
|
|
245
|
+
*/
|
|
246
|
+
export function enableStrictMode(): void {
|
|
247
|
+
_strictMode = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Disable strict capability enforcement (return to lax mode).
|
|
252
|
+
* Primarily useful for testing.
|
|
253
|
+
*/
|
|
254
|
+
export function disableStrictMode(): void {
|
|
255
|
+
_strictMode = false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Set the native capability module.
|
|
260
|
+
* Called by the native layer during runtime initialization.
|
|
261
|
+
*/
|
|
262
|
+
export function setNativeCapabilityModule(module: NativeCapabilityModule): void {
|
|
263
|
+
_nativeCapabilityModule = module;
|
|
264
|
+
|
|
265
|
+
// Register audit callback forwarding
|
|
266
|
+
if (module.onAuditEvent) {
|
|
267
|
+
module.onAuditEvent((event) => {
|
|
268
|
+
_auditCallbacks.forEach(cb => {
|
|
269
|
+
try {
|
|
270
|
+
cb(event);
|
|
271
|
+
} catch (e) {
|
|
272
|
+
console.error('Capability audit callback error:', e);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get the native capability module.
|
|
281
|
+
*/
|
|
282
|
+
export function getNativeCapabilityModule(): NativeCapabilityModule | null {
|
|
283
|
+
return _nativeCapabilityModule;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Options for test mode.
|
|
288
|
+
*/
|
|
289
|
+
export interface TestModeOptions {
|
|
290
|
+
/** Capability grants to pre-configure */
|
|
291
|
+
grants?: Capability[];
|
|
292
|
+
/** Suppress security warnings in console (default: true) */
|
|
293
|
+
silent?: boolean;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Enable test mode with specified grants.
|
|
298
|
+
* WARNING: Only use in test environments!
|
|
299
|
+
*
|
|
300
|
+
* @param grantsOrOptions - Either an array of grants (legacy) or options object
|
|
301
|
+
*/
|
|
302
|
+
export function enableTestMode(grantsOrOptions: Capability[] | TestModeOptions = {}): void {
|
|
303
|
+
if (_strictMode) {
|
|
304
|
+
throw new Error('Cannot enable capability test mode when strict mode is enabled');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Handle legacy array signature
|
|
308
|
+
if (Array.isArray(grantsOrOptions)) {
|
|
309
|
+
_testMode = true;
|
|
310
|
+
_testGrants = new Set(grantsOrOptions);
|
|
311
|
+
_silentMode = true; // Default to silent for legacy callers (tests)
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const options = grantsOrOptions;
|
|
316
|
+
_testMode = true;
|
|
317
|
+
_testGrants = new Set(options.grants ?? []);
|
|
318
|
+
_silentMode = options.silent ?? true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Disable test mode.
|
|
323
|
+
*/
|
|
324
|
+
export function disableTestMode(): void {
|
|
325
|
+
_testMode = false;
|
|
326
|
+
_testGrants.clear();
|
|
327
|
+
_silentMode = false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Add a capability grant in test mode.
|
|
332
|
+
*/
|
|
333
|
+
export function addTestGrant(capability: Capability): void {
|
|
334
|
+
if (!_testMode) {
|
|
335
|
+
throw new Error('Test mode not enabled');
|
|
336
|
+
}
|
|
337
|
+
_testGrants.add(capability);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ============================================================================
|
|
341
|
+
// Capability Checking
|
|
342
|
+
// ============================================================================
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Check if a capability is granted.
|
|
346
|
+
*
|
|
347
|
+
* @param capability The capability to check (e.g., "network:fetch", "device:location")
|
|
348
|
+
* @param moduleId Optional module ID for per-module checks
|
|
349
|
+
* @returns CapabilityCheckResult with allowed status and denial reason
|
|
350
|
+
*/
|
|
351
|
+
export function checkCapability(capability: Capability, moduleId?: number): CapabilityCheckResult {
|
|
352
|
+
// Fast path: if moduleId provided and we have a cached bitmask
|
|
353
|
+
if (moduleId !== undefined && !_testMode) {
|
|
354
|
+
const bit = getCapabilityBit(capability);
|
|
355
|
+
if (bit !== undefined) {
|
|
356
|
+
const bits = _effectiveBits.get(moduleId);
|
|
357
|
+
if (bits) {
|
|
358
|
+
const gen = _cachedGeneration.get(moduleId);
|
|
359
|
+
if (gen === _permissionGeneration) {
|
|
360
|
+
const allowed = bit < 32
|
|
361
|
+
? (bits[0] & (1 << bit)) !== 0
|
|
362
|
+
: (bits[1] & (1 << (bit - 32))) !== 0;
|
|
363
|
+
if (allowed) {
|
|
364
|
+
return { allowed: true, capability };
|
|
365
|
+
}
|
|
366
|
+
// Fall through to slow path for detailed denial reason
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Test mode - check in-memory grants
|
|
373
|
+
if (_testMode) {
|
|
374
|
+
const allowed = _testGrants.has(capability) ||
|
|
375
|
+
_testGrants.has('*') ||
|
|
376
|
+
_testGrants.has(capability.split(':')[0] + ':*');
|
|
377
|
+
|
|
378
|
+
logAuditEvent({
|
|
379
|
+
event: allowed ? 'capability_granted' : 'capability_denied',
|
|
380
|
+
action: 'check',
|
|
381
|
+
capability,
|
|
382
|
+
moduleId,
|
|
383
|
+
source: 'module',
|
|
384
|
+
reason: allowed ? undefined : 'Test mode: capability not in grants',
|
|
385
|
+
timestamp: Date.now(),
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
allowed,
|
|
390
|
+
reason: allowed ? undefined : 'module_not_granted',
|
|
391
|
+
capability,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Native module - delegate to native layer
|
|
396
|
+
if (_nativeCapabilityModule) {
|
|
397
|
+
const allowed = _nativeCapabilityModule.checkCapability(capability, moduleId);
|
|
398
|
+
return {
|
|
399
|
+
allowed,
|
|
400
|
+
reason: allowed ? undefined : 'module_not_granted',
|
|
401
|
+
capability,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// No native module and not in test mode
|
|
406
|
+
// Behavior depends on strict mode:
|
|
407
|
+
// - Strict mode (opt-in): deny all - secure for production
|
|
408
|
+
// - Lax mode (default): allow all - friendly for development
|
|
409
|
+
|
|
410
|
+
if (_strictMode) {
|
|
411
|
+
if (!_silentMode) {
|
|
412
|
+
console.warn(`[Security] Capability denied (strict mode): ${capability}`);
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
allowed: false,
|
|
416
|
+
reason: 'app_not_granted',
|
|
417
|
+
capability,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Lax mode - allow with warning (only warn once per capability to reduce noise)
|
|
422
|
+
if (!_silentMode && !_warnedCapabilities.has(capability)) {
|
|
423
|
+
_warnedCapabilities.add(capability);
|
|
424
|
+
console.warn(
|
|
425
|
+
`[Security] No capability module available. ` +
|
|
426
|
+
`Allowing "${capability}" in lax mode. ` +
|
|
427
|
+
`Call enableStrictMode() for production security.`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
allowed: true,
|
|
433
|
+
capability,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Require a capability, throwing CapabilityDeniedError if not granted.
|
|
439
|
+
* Use this for synchronous capability checks.
|
|
440
|
+
*
|
|
441
|
+
* @param capability The capability to require
|
|
442
|
+
* @param moduleId Optional module ID
|
|
443
|
+
* @throws CapabilityDeniedError if capability is denied
|
|
444
|
+
*/
|
|
445
|
+
export function requireCapability(capability: Capability, moduleId?: number): void {
|
|
446
|
+
const result = checkCapability(capability, moduleId);
|
|
447
|
+
if (!result.allowed) {
|
|
448
|
+
throw new CapabilityDeniedError(
|
|
449
|
+
capability,
|
|
450
|
+
result.reason,
|
|
451
|
+
result.canRequestAgain,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Request a capability from the broker.
|
|
458
|
+
* This may trigger a user permission prompt.
|
|
459
|
+
*
|
|
460
|
+
* @param capability The capability to request
|
|
461
|
+
* @returns Promise resolving to true if granted, false if denied
|
|
462
|
+
*/
|
|
463
|
+
export async function requestCapability(capability: Capability): Promise<boolean> {
|
|
464
|
+
// Test mode
|
|
465
|
+
if (_testMode) {
|
|
466
|
+
return _testGrants.has(capability) ||
|
|
467
|
+
_testGrants.has('*') ||
|
|
468
|
+
_testGrants.has(capability.split(':')[0] + ':*');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Native module
|
|
472
|
+
if (_nativeCapabilityModule) {
|
|
473
|
+
return _nativeCapabilityModule.requestCapability(capability);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// No native module - behavior depends on strict mode
|
|
477
|
+
return !_strictMode;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get all currently granted capabilities.
|
|
482
|
+
*/
|
|
483
|
+
export function getGrantedCapabilities(): Capability[] {
|
|
484
|
+
if (_testMode) {
|
|
485
|
+
return Array.from(_testGrants);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (_nativeCapabilityModule) {
|
|
489
|
+
return _nativeCapabilityModule.getGrantedCapabilities();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return [];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ============================================================================
|
|
496
|
+
// Audit Logging
|
|
497
|
+
// ============================================================================
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Register a callback for capability audit events.
|
|
501
|
+
*
|
|
502
|
+
* @param callback Function called for each audit event
|
|
503
|
+
* @returns Unsubscribe function
|
|
504
|
+
*/
|
|
505
|
+
export function onCapabilityAudit(
|
|
506
|
+
callback: (event: CapabilityAuditEvent) => void
|
|
507
|
+
): () => void {
|
|
508
|
+
_auditCallbacks.push(callback);
|
|
509
|
+
return () => {
|
|
510
|
+
const index = _auditCallbacks.indexOf(callback);
|
|
511
|
+
if (index !== -1) {
|
|
512
|
+
_auditCallbacks.splice(index, 1);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Log an audit event.
|
|
519
|
+
* Called internally and can be called by native layer.
|
|
520
|
+
*/
|
|
521
|
+
export function logAuditEvent(event: CapabilityAuditEvent): void {
|
|
522
|
+
_auditCallbacks.forEach(cb => {
|
|
523
|
+
try {
|
|
524
|
+
cb(event);
|
|
525
|
+
} catch (e) {
|
|
526
|
+
console.error('Capability audit callback error:', e);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ============================================================================
|
|
532
|
+
// Capability Introspection (Future-Proofing)
|
|
533
|
+
// ============================================================================
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Permission status for a capability.
|
|
537
|
+
* Mirrors common OS permission states.
|
|
538
|
+
*/
|
|
539
|
+
export type CapabilityStatus =
|
|
540
|
+
| 'granted' // Permission is granted
|
|
541
|
+
| 'denied' // Permission was explicitly denied
|
|
542
|
+
| 'not_determined' // User hasn't been asked yet
|
|
543
|
+
| 'restricted' // System policy prevents access
|
|
544
|
+
| 'limited' // Partial access (e.g., limited photo selection)
|
|
545
|
+
| 'unknown'; // Status cannot be determined
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Detailed information about a capability.
|
|
549
|
+
* Use this to understand what a capability is and how to handle it.
|
|
550
|
+
*/
|
|
551
|
+
export interface CapabilityInfo {
|
|
552
|
+
/** The capability string */
|
|
553
|
+
capability: Capability;
|
|
554
|
+
|
|
555
|
+
/** Whether this capability is recognized by the runtime */
|
|
556
|
+
exists: boolean;
|
|
557
|
+
|
|
558
|
+
/** Current permission status */
|
|
559
|
+
status: CapabilityStatus;
|
|
560
|
+
|
|
561
|
+
/** Whether the app can request this capability (show permission prompt) */
|
|
562
|
+
canRequest: boolean;
|
|
563
|
+
|
|
564
|
+
/** Whether the capability is currently usable */
|
|
565
|
+
isUsable: boolean;
|
|
566
|
+
|
|
567
|
+
/** Minimum OS version required (e.g., "iOS 14.0", "Android 13") */
|
|
568
|
+
minimumOSVersion?: string;
|
|
569
|
+
|
|
570
|
+
/** Current OS version */
|
|
571
|
+
currentOSVersion?: string;
|
|
572
|
+
|
|
573
|
+
/** If deprecated, what capability replaces this one */
|
|
574
|
+
replacedBy?: Capability[];
|
|
575
|
+
|
|
576
|
+
/** If this is a new capability, what it replaces */
|
|
577
|
+
replaces?: Capability[];
|
|
578
|
+
|
|
579
|
+
/** Human-readable description of what this capability grants */
|
|
580
|
+
description?: string;
|
|
581
|
+
|
|
582
|
+
/** URL to open system settings for this capability */
|
|
583
|
+
settingsUrl?: string;
|
|
584
|
+
|
|
585
|
+
/** Additional platform-specific metadata */
|
|
586
|
+
metadata?: Record<string, unknown>;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Information about the capability system itself.
|
|
591
|
+
*/
|
|
592
|
+
export interface CapabilitySystemInfo {
|
|
593
|
+
/** Version of the capability API */
|
|
594
|
+
apiVersion: string;
|
|
595
|
+
|
|
596
|
+
/** List of all known capabilities */
|
|
597
|
+
knownCapabilities: Capability[];
|
|
598
|
+
|
|
599
|
+
/** Operating system name */
|
|
600
|
+
osName: 'ios' | 'android' | 'web' | 'windows' | 'macos' | 'unknown';
|
|
601
|
+
|
|
602
|
+
/** Operating system version */
|
|
603
|
+
osVersion: string;
|
|
604
|
+
|
|
605
|
+
/** Ibex runtime version */
|
|
606
|
+
runtimeVersion: string;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Extended native module interface for introspection.
|
|
611
|
+
* Native implementations should implement these for full future-proofing support.
|
|
612
|
+
*/
|
|
613
|
+
export interface NativeCapabilityModuleV2 extends NativeCapabilityModule {
|
|
614
|
+
/** Get detailed information about a capability */
|
|
615
|
+
getCapabilityInfo?(capability: Capability): CapabilityInfo;
|
|
616
|
+
|
|
617
|
+
/** Get system information */
|
|
618
|
+
getSystemInfo?(): CapabilitySystemInfo;
|
|
619
|
+
|
|
620
|
+
/** Get capabilities required by an API */
|
|
621
|
+
getRequiredCapabilities?(apiName: string): Capability[];
|
|
622
|
+
|
|
623
|
+
/** Check if a capability exists (is known to the system) */
|
|
624
|
+
capabilityExists?(capability: Capability): boolean;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Current API version - bump when making breaking changes
|
|
628
|
+
const CAPABILITY_API_VERSION = '1.0.0';
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Get detailed information about a capability.
|
|
632
|
+
*
|
|
633
|
+
* Use this to:
|
|
634
|
+
* - Check if a capability exists before using it
|
|
635
|
+
* - Determine if user can be prompted for permission
|
|
636
|
+
* - Get the settings URL to direct users to enable permission
|
|
637
|
+
* - Check OS version requirements
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* ```ts
|
|
641
|
+
* const info = getCapabilityInfo('device:location:precise');
|
|
642
|
+
* if (!info.exists) {
|
|
643
|
+
* // Fall back to coarse location
|
|
644
|
+
* const coarseInfo = getCapabilityInfo('device:location');
|
|
645
|
+
* // ...
|
|
646
|
+
* } else if (info.status === 'denied' && !info.canRequest) {
|
|
647
|
+
* // Direct user to settings
|
|
648
|
+
* showSettingsPrompt(info.settingsUrl);
|
|
649
|
+
* }
|
|
650
|
+
* ```
|
|
651
|
+
*/
|
|
652
|
+
export function getCapabilityInfo(capability: Capability): CapabilityInfo {
|
|
653
|
+
// Check if native module supports introspection
|
|
654
|
+
const module = _nativeCapabilityModule as NativeCapabilityModuleV2 | null;
|
|
655
|
+
if (module?.getCapabilityInfo) {
|
|
656
|
+
return module.getCapabilityInfo(capability);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Fallback implementation for testing and development
|
|
660
|
+
const isKnown = isKnownCapability(capability);
|
|
661
|
+
const checkResult = checkCapability(capability);
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
capability,
|
|
665
|
+
exists: isKnown,
|
|
666
|
+
status: checkResult.allowed ? 'granted' : 'not_determined',
|
|
667
|
+
canRequest: isKnown && !checkResult.allowed,
|
|
668
|
+
isUsable: checkResult.allowed,
|
|
669
|
+
description: getCapabilityDescription(capability),
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Get information about the capability system.
|
|
675
|
+
*/
|
|
676
|
+
export function getCapabilitySystemInfo(): CapabilitySystemInfo {
|
|
677
|
+
const module = _nativeCapabilityModule as NativeCapabilityModuleV2 | null;
|
|
678
|
+
if (module?.getSystemInfo) {
|
|
679
|
+
return module.getSystemInfo();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const fallback = getFallbackCapabilitySystemInfo();
|
|
683
|
+
|
|
684
|
+
// Fallback for testing
|
|
685
|
+
return {
|
|
686
|
+
apiVersion: CAPABILITY_API_VERSION,
|
|
687
|
+
knownCapabilities: Object.values(Capabilities),
|
|
688
|
+
osName: fallback.osName,
|
|
689
|
+
osVersion: fallback.osVersion,
|
|
690
|
+
runtimeVersion: fallback.runtimeVersion,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function getFallbackCapabilitySystemInfo(): Pick<
|
|
695
|
+
CapabilitySystemInfo,
|
|
696
|
+
'osName' | 'osVersion' | 'runtimeVersion'
|
|
697
|
+
> {
|
|
698
|
+
const globalHints = globalThis as {
|
|
699
|
+
__exactPlatform?: unknown;
|
|
700
|
+
__exactPlatformVersion?: unknown;
|
|
701
|
+
process?: {
|
|
702
|
+
platform?: unknown;
|
|
703
|
+
version?: unknown;
|
|
704
|
+
__exactOSRelease?: unknown;
|
|
705
|
+
__exactOSVersion?: unknown;
|
|
706
|
+
};
|
|
707
|
+
};
|
|
708
|
+
const processHints =
|
|
709
|
+
globalHints.process && typeof globalHints.process === 'object'
|
|
710
|
+
? globalHints.process
|
|
711
|
+
: undefined;
|
|
712
|
+
const platform = readNonEmptyString(globalHints.__exactPlatform) ??
|
|
713
|
+
readNonEmptyString(processHints?.platform);
|
|
714
|
+
const osName = normalizeCapabilityOSName(platform);
|
|
715
|
+
|
|
716
|
+
// @ref LLP 0008#os-info - Android permission diagnostics use the Java host SDK version.
|
|
717
|
+
const osVersion = osName === 'android'
|
|
718
|
+
? readNonEmptyString(globalHints.__exactPlatformVersion) ??
|
|
719
|
+
readNonEmptyString(processHints?.__exactOSRelease) ??
|
|
720
|
+
stripAndroidVersionPrefix(readNonEmptyString(processHints?.__exactOSVersion)) ??
|
|
721
|
+
'unknown'
|
|
722
|
+
: 'unknown';
|
|
723
|
+
|
|
724
|
+
return {
|
|
725
|
+
osName,
|
|
726
|
+
osVersion,
|
|
727
|
+
runtimeVersion: readNonEmptyString(processHints?.version) ?? 'development',
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function readNonEmptyString(value: unknown): string | undefined {
|
|
732
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function stripAndroidVersionPrefix(value: string | undefined): string | undefined {
|
|
736
|
+
if (!value) return undefined;
|
|
737
|
+
return value.replace(/^Android\s+/, '');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function normalizeCapabilityOSName(platform: string | undefined): CapabilitySystemInfo['osName'] {
|
|
741
|
+
return platform === 'android' ? 'android' : 'unknown';
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Get the capabilities required by a specific API.
|
|
746
|
+
*
|
|
747
|
+
* Use this to understand what permissions an API needs before calling it,
|
|
748
|
+
* allowing you to request permissions proactively or show appropriate UI.
|
|
749
|
+
*
|
|
750
|
+
* @example
|
|
751
|
+
* ```ts
|
|
752
|
+
* const required = getRequiredCapabilities('Geolocation.getCurrentPosition');
|
|
753
|
+
* for (const cap of required) {
|
|
754
|
+
* const info = getCapabilityInfo(cap);
|
|
755
|
+
* if (!info.isUsable && info.canRequest) {
|
|
756
|
+
* await requestCapability(cap);
|
|
757
|
+
* }
|
|
758
|
+
* }
|
|
759
|
+
* ```
|
|
760
|
+
*/
|
|
761
|
+
export function getRequiredCapabilities(apiName: string): Capability[] {
|
|
762
|
+
const module = _nativeCapabilityModule as NativeCapabilityModuleV2 | null;
|
|
763
|
+
if (module?.getRequiredCapabilities) {
|
|
764
|
+
return module.getRequiredCapabilities(apiName);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Fallback: built-in API to capability mapping
|
|
768
|
+
const apiCapabilityMap = getApiCapabilityMap();
|
|
769
|
+
return apiCapabilityMap[apiName] ?? [];
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Check if a capability is known to the current runtime.
|
|
774
|
+
*
|
|
775
|
+
* Use this to check for capabilities that may not exist on older OS versions
|
|
776
|
+
* or runtime versions before attempting to use them.
|
|
777
|
+
*/
|
|
778
|
+
export function isKnownCapability(capability: Capability): boolean {
|
|
779
|
+
const module = _nativeCapabilityModule as NativeCapabilityModuleV2 | null;
|
|
780
|
+
if (module?.capabilityExists) {
|
|
781
|
+
return module.capabilityExists(capability);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Fallback: check against known capabilities
|
|
785
|
+
const knownValues = Object.values(Capabilities) as string[];
|
|
786
|
+
if (knownValues.includes(capability)) {
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Check if it's a parameterized version of a known capability
|
|
791
|
+
// e.g., "network:fetch:api.example.com" matches "network:fetch"
|
|
792
|
+
const { category, action } = parseCapability(capability);
|
|
793
|
+
const baseCapability = `${category}:${action}`;
|
|
794
|
+
return knownValues.includes(baseCapability);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Get the built-in mapping of API names to required capabilities.
|
|
799
|
+
* Uses a getter function to avoid circular dependency issues.
|
|
800
|
+
*/
|
|
801
|
+
function getApiCapabilityMap(): Record<string, Capability[]> {
|
|
802
|
+
return {
|
|
803
|
+
// Fetch API
|
|
804
|
+
'fetch': ['network:fetch'],
|
|
805
|
+
'Request': ['network:fetch'],
|
|
806
|
+
|
|
807
|
+
// WebSocket
|
|
808
|
+
'WebSocket': ['network:connect'],
|
|
809
|
+
|
|
810
|
+
// Storage
|
|
811
|
+
'localStorage': ['storage:local'],
|
|
812
|
+
'localStorage.getItem': ['storage:local'],
|
|
813
|
+
'localStorage.setItem': ['storage:local'],
|
|
814
|
+
'sessionStorage': ['storage:session'],
|
|
815
|
+
|
|
816
|
+
// Crypto
|
|
817
|
+
'crypto.getRandomValues': ['crypto:random'],
|
|
818
|
+
'crypto.randomUUID': ['crypto:random'],
|
|
819
|
+
'crypto.subtle': ['crypto:subtle'],
|
|
820
|
+
'crypto.subtle.encrypt': ['crypto:subtle'],
|
|
821
|
+
'crypto.subtle.decrypt': ['crypto:subtle'],
|
|
822
|
+
'crypto.subtle.sign': ['crypto:subtle'],
|
|
823
|
+
'crypto.subtle.verify': ['crypto:subtle'],
|
|
824
|
+
|
|
825
|
+
// Geolocation
|
|
826
|
+
'Geolocation.getCurrentPosition': ['device:location'],
|
|
827
|
+
'Geolocation.watchPosition': ['device:location'],
|
|
828
|
+
|
|
829
|
+
// Clipboard
|
|
830
|
+
'navigator.clipboard.read': ['clipboard:read'],
|
|
831
|
+
'navigator.clipboard.readText': ['clipboard:read'],
|
|
832
|
+
'navigator.clipboard.write': ['clipboard:write'],
|
|
833
|
+
'navigator.clipboard.writeText': ['clipboard:write'],
|
|
834
|
+
|
|
835
|
+
// Notifications
|
|
836
|
+
'Notification': ['device:notifications'],
|
|
837
|
+
'Notification.requestPermission': ['device:notifications'],
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Get a human-readable description of a capability.
|
|
843
|
+
*/
|
|
844
|
+
function getCapabilityDescription(capability: Capability): string {
|
|
845
|
+
const descriptions: Record<string, string> = {
|
|
846
|
+
'network:fetch': 'Make network requests to remote servers',
|
|
847
|
+
'network:connect': 'Establish persistent network connections',
|
|
848
|
+
'network:local': 'Access devices on your local network',
|
|
849
|
+
'storage:local': 'Store data locally on your device',
|
|
850
|
+
'storage:session': 'Store temporary session data',
|
|
851
|
+
'device:location': 'Access your location',
|
|
852
|
+
'device:location:precise': 'Access your precise location',
|
|
853
|
+
'device:location:whenInUse': 'Access location while using the app',
|
|
854
|
+
'device:location:always': 'Access location even in background',
|
|
855
|
+
'device:camera': 'Access your camera',
|
|
856
|
+
'device:microphone': 'Access your microphone',
|
|
857
|
+
'device:photos': 'Access your photos',
|
|
858
|
+
'device:contacts': 'Access your contacts',
|
|
859
|
+
'device:bluetooth': 'Use Bluetooth',
|
|
860
|
+
'device:biometrics': 'Use Face ID or fingerprint',
|
|
861
|
+
'device:notifications': 'Send you notifications',
|
|
862
|
+
'crypto:random': 'Generate random numbers',
|
|
863
|
+
'crypto:subtle': 'Use cryptographic operations',
|
|
864
|
+
'clipboard:read': 'Read from clipboard',
|
|
865
|
+
'clipboard:write': 'Write to clipboard',
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
return descriptions[capability] ?? `Access ${capability}`;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ============================================================================
|
|
872
|
+
// Bitset Fast Path
|
|
873
|
+
// ============================================================================
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Get the bit position for a capability.
|
|
877
|
+
* For parameterized caps like "network:fetch:api.example.com",
|
|
878
|
+
* extracts base "network:fetch" and looks up its bit.
|
|
879
|
+
*
|
|
880
|
+
* @returns Bit position (0-55) or undefined if not a well-known capability
|
|
881
|
+
*/
|
|
882
|
+
export function getCapabilityBit(capability: Capability): number | undefined {
|
|
883
|
+
// Fast path: direct lookup
|
|
884
|
+
const direct = CapabilityBit[capability];
|
|
885
|
+
if (direct !== undefined) return direct;
|
|
886
|
+
|
|
887
|
+
// Parameterized capability: extract base
|
|
888
|
+
const { category, action } = parseCapability(capability);
|
|
889
|
+
return CapabilityBit[`${category}:${action}`];
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Module effective bitmasks: moduleId -> [lo32, hi32]
|
|
894
|
+
* Precomputed intersection of all 4 security layers for each module.
|
|
895
|
+
*/
|
|
896
|
+
const _effectiveBits: Map<number, [number, number]> = new Map();
|
|
897
|
+
|
|
898
|
+
/** Generation counter for permission changes */
|
|
899
|
+
let _permissionGeneration = 0;
|
|
900
|
+
|
|
901
|
+
/** Per-module cached generation */
|
|
902
|
+
const _cachedGeneration: Map<number, number> = new Map();
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Get the current permission generation counter.
|
|
906
|
+
* Increments whenever permissions change, invalidating cached bitmasks.
|
|
907
|
+
*/
|
|
908
|
+
export function getPermissionGeneration(): number {
|
|
909
|
+
return _permissionGeneration;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Increment the permission generation counter.
|
|
914
|
+
* Call this when any permission layer changes (OS, app, broker, or module grants).
|
|
915
|
+
*/
|
|
916
|
+
export function invalidatePermissionCache(): void {
|
|
917
|
+
_permissionGeneration++;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Set the effective capability bitmask for a module.
|
|
922
|
+
* Called at module load time with the precomputed intersection of all 4 layers.
|
|
923
|
+
*
|
|
924
|
+
* @param moduleId The module's numeric ID
|
|
925
|
+
* @param capabilities Array of granted capability strings
|
|
926
|
+
*/
|
|
927
|
+
export function setModuleCapabilities(moduleId: number, capabilities: Capability[]): void {
|
|
928
|
+
// Grant capabilities through native layer
|
|
929
|
+
if (typeof (globalThis as any).__exactGrantCapability === 'function') {
|
|
930
|
+
for (const cap of capabilities) {
|
|
931
|
+
(globalThis as any).__exactGrantCapability(moduleId, cap);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Set up fast-path bitmask cache
|
|
936
|
+
let lo = 0;
|
|
937
|
+
let hi = 0;
|
|
938
|
+
for (const cap of capabilities) {
|
|
939
|
+
const bit = getCapabilityBit(cap);
|
|
940
|
+
if (bit !== undefined) {
|
|
941
|
+
if (bit < 32) {
|
|
942
|
+
lo |= (1 << bit);
|
|
943
|
+
} else {
|
|
944
|
+
hi |= (1 << (bit - 32));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
_effectiveBits.set(moduleId, [lo, hi]);
|
|
949
|
+
_cachedGeneration.set(moduleId, _permissionGeneration);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Clear capability bitmask for a module.
|
|
954
|
+
* Call when a module is unloaded.
|
|
955
|
+
*/
|
|
956
|
+
export function clearModuleCapabilities(moduleId: number): void {
|
|
957
|
+
_effectiveBits.delete(moduleId);
|
|
958
|
+
_cachedGeneration.delete(moduleId);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Fast boolean capability check — no allocation on the success path.
|
|
963
|
+
* Returns true if the module has the capability, false otherwise.
|
|
964
|
+
*
|
|
965
|
+
* This is the hot path: a single Map lookup + bitwise AND.
|
|
966
|
+
*
|
|
967
|
+
* @param moduleId The module's numeric ID
|
|
968
|
+
* @param capability The capability to check
|
|
969
|
+
* @returns true if allowed, false if denied or unknown
|
|
970
|
+
*/
|
|
971
|
+
export function checkCapabilityFast(moduleId: number, capability: Capability): boolean {
|
|
972
|
+
const bit = getCapabilityBit(capability);
|
|
973
|
+
if (bit === undefined) return false; // unknown capability
|
|
974
|
+
|
|
975
|
+
const bits = _effectiveBits.get(moduleId);
|
|
976
|
+
if (!bits) return false;
|
|
977
|
+
|
|
978
|
+
// Check generation
|
|
979
|
+
const gen = _cachedGeneration.get(moduleId);
|
|
980
|
+
if (gen !== _permissionGeneration) return false; // stale cache
|
|
981
|
+
|
|
982
|
+
if (bit < 32) {
|
|
983
|
+
return (bits[0] & (1 << bit)) !== 0;
|
|
984
|
+
}
|
|
985
|
+
return (bits[1] & (1 << (bit - 32))) !== 0;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// ============================================================================
|
|
989
|
+
// Capability Helpers
|
|
990
|
+
// ============================================================================
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Parse a capability string into category and action.
|
|
994
|
+
*
|
|
995
|
+
* @param capability Full capability string (e.g., "network:fetch:api.example.com")
|
|
996
|
+
* @returns Parsed capability parts
|
|
997
|
+
*/
|
|
998
|
+
export function parseCapability(capability: Capability): {
|
|
999
|
+
category: string;
|
|
1000
|
+
action: string;
|
|
1001
|
+
resource?: string;
|
|
1002
|
+
} {
|
|
1003
|
+
const parts = capability.split(':');
|
|
1004
|
+
return {
|
|
1005
|
+
category: parts[0] || '',
|
|
1006
|
+
action: parts[1] || '',
|
|
1007
|
+
resource: parts[2],
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Check if a capability matches a grant pattern.
|
|
1013
|
+
* Supports wildcards like "network:*" or "*".
|
|
1014
|
+
*
|
|
1015
|
+
* @param capability The capability to check
|
|
1016
|
+
* @param grant The grant pattern to match against
|
|
1017
|
+
*/
|
|
1018
|
+
export function capabilityMatches(capability: Capability, grant: Capability): boolean {
|
|
1019
|
+
if (grant === '*') return true;
|
|
1020
|
+
if (grant === capability) return true;
|
|
1021
|
+
|
|
1022
|
+
const capParts = capability.split(':');
|
|
1023
|
+
const grantParts = grant.split(':');
|
|
1024
|
+
|
|
1025
|
+
// Check each part
|
|
1026
|
+
for (let i = 0; i < grantParts.length; i++) {
|
|
1027
|
+
if (grantParts[i] === '*') return true;
|
|
1028
|
+
if (grantParts[i] !== capParts[i]) return false;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Grant must be at least as specific as capability
|
|
1032
|
+
return grantParts.length <= capParts.length;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ============================================================================
|
|
1036
|
+
// Common Capability Constants
|
|
1037
|
+
// ============================================================================
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Well-known capability names for type safety.
|
|
1041
|
+
* @see OS_CAPABILITY_SECURITY_OVERVIEW.md
|
|
1042
|
+
* @see JS_CAPABILITY_SECURITY_MODEL_SPEC_AND_PLAN.md
|
|
1043
|
+
*/
|
|
1044
|
+
export const Capabilities = {
|
|
1045
|
+
// Network
|
|
1046
|
+
NETWORK_FETCH: 'network:fetch',
|
|
1047
|
+
NETWORK_CONNECT: 'network:connect',
|
|
1048
|
+
NETWORK_LISTEN: 'network:listen',
|
|
1049
|
+
NETWORK_LOCAL: 'network:local', // Local network access (mDNS, Bonjour)
|
|
1050
|
+
|
|
1051
|
+
// File System
|
|
1052
|
+
FS_READ: 'fs:read',
|
|
1053
|
+
FS_WRITE: 'fs:write',
|
|
1054
|
+
FS_LIST: 'fs:list',
|
|
1055
|
+
FS_WATCH: 'fs:watch',
|
|
1056
|
+
|
|
1057
|
+
// Environment
|
|
1058
|
+
ENV_READ: 'env:read',
|
|
1059
|
+
|
|
1060
|
+
// Process
|
|
1061
|
+
PROCESS_SPAWN: 'process:spawn',
|
|
1062
|
+
PROCESS_SIGNAL: 'process:signal',
|
|
1063
|
+
PROCESS_CWD: 'process:cwd',
|
|
1064
|
+
|
|
1065
|
+
// Device - Location (granular per OS_CAPABILITY_SECURITY_OVERVIEW.md Section 7.1)
|
|
1066
|
+
DEVICE_LOCATION: 'device:location',
|
|
1067
|
+
DEVICE_LOCATION_WHEN_IN_USE: 'device:location:whenInUse',
|
|
1068
|
+
DEVICE_LOCATION_ALWAYS: 'device:location:always',
|
|
1069
|
+
DEVICE_LOCATION_PRECISE: 'device:location:precise',
|
|
1070
|
+
DEVICE_LOCATION_REDUCED: 'device:location:reduced',
|
|
1071
|
+
|
|
1072
|
+
// Device - Camera/Microphone
|
|
1073
|
+
DEVICE_CAMERA: 'device:camera',
|
|
1074
|
+
DEVICE_MICROPHONE: 'device:microphone',
|
|
1075
|
+
|
|
1076
|
+
// Device - Photos (granular per OS_CAPABILITY_SECURITY_OVERVIEW.md Section 7.3)
|
|
1077
|
+
DEVICE_PHOTOS: 'device:photos',
|
|
1078
|
+
DEVICE_PHOTOS_READ: 'device:photos:read',
|
|
1079
|
+
DEVICE_PHOTOS_WRITE: 'device:photos:write',
|
|
1080
|
+
DEVICE_PHOTOS_LIMITED: 'device:photos:limited',
|
|
1081
|
+
|
|
1082
|
+
// Device - Contacts
|
|
1083
|
+
DEVICE_CONTACTS: 'device:contacts',
|
|
1084
|
+
DEVICE_CONTACTS_READ: 'device:contacts:read',
|
|
1085
|
+
DEVICE_CONTACTS_WRITE: 'device:contacts:write',
|
|
1086
|
+
|
|
1087
|
+
// Device - Sensors
|
|
1088
|
+
DEVICE_SENSORS: 'device:sensors',
|
|
1089
|
+
DEVICE_MOTION: 'device:motion',
|
|
1090
|
+
|
|
1091
|
+
// Device - Bluetooth (granular per OS_CAPABILITY_SECURITY_OVERVIEW.md Section 7.5)
|
|
1092
|
+
DEVICE_BLUETOOTH: 'device:bluetooth',
|
|
1093
|
+
DEVICE_BLUETOOTH_SCAN: 'device:bluetooth:scan',
|
|
1094
|
+
DEVICE_BLUETOOTH_CONNECT: 'device:bluetooth:connect',
|
|
1095
|
+
DEVICE_BLUETOOTH_ADVERTISE: 'device:bluetooth:advertise',
|
|
1096
|
+
|
|
1097
|
+
// Device - Biometrics (Face ID, Touch ID, fingerprint)
|
|
1098
|
+
DEVICE_BIOMETRICS: 'device:biometrics',
|
|
1099
|
+
|
|
1100
|
+
// Device - Credentials (Keychain/Credential Manager)
|
|
1101
|
+
DEVICE_CREDENTIALS: 'device:credentials',
|
|
1102
|
+
DEVICE_CREDENTIALS_READ: 'device:credentials:read',
|
|
1103
|
+
DEVICE_CREDENTIALS_WRITE: 'device:credentials:write',
|
|
1104
|
+
|
|
1105
|
+
// Device - Notifications
|
|
1106
|
+
DEVICE_NOTIFICATIONS: 'device:notifications',
|
|
1107
|
+
|
|
1108
|
+
// Device - Calendar
|
|
1109
|
+
DEVICE_CALENDAR: 'device:calendar',
|
|
1110
|
+
DEVICE_CALENDAR_READ: 'device:calendar:read',
|
|
1111
|
+
DEVICE_CALENDAR_WRITE: 'device:calendar:write',
|
|
1112
|
+
|
|
1113
|
+
// Device - Reminders
|
|
1114
|
+
DEVICE_REMINDERS: 'device:reminders',
|
|
1115
|
+
|
|
1116
|
+
// Device - Health (HealthKit/Health Connect)
|
|
1117
|
+
DEVICE_HEALTH: 'device:health',
|
|
1118
|
+
DEVICE_HEALTH_READ: 'device:health:read',
|
|
1119
|
+
DEVICE_HEALTH_WRITE: 'device:health:write',
|
|
1120
|
+
|
|
1121
|
+
// Storage
|
|
1122
|
+
STORAGE_LOCAL: 'storage:local',
|
|
1123
|
+
STORAGE_SESSION: 'storage:session',
|
|
1124
|
+
STORAGE_PERSIST: 'storage:persist', // Request persistent storage
|
|
1125
|
+
|
|
1126
|
+
// SQLite
|
|
1127
|
+
SQLITE_READ: 'sqlite:read',
|
|
1128
|
+
SQLITE_WRITE: 'sqlite:write',
|
|
1129
|
+
|
|
1130
|
+
// Clipboard
|
|
1131
|
+
CLIPBOARD_READ: 'clipboard:read',
|
|
1132
|
+
CLIPBOARD_WRITE: 'clipboard:write',
|
|
1133
|
+
|
|
1134
|
+
// Crypto
|
|
1135
|
+
CRYPTO_RANDOM: 'crypto:random',
|
|
1136
|
+
CRYPTO_SUBTLE: 'crypto:subtle',
|
|
1137
|
+
|
|
1138
|
+
// Time
|
|
1139
|
+
TIME_NOW: 'time:now',
|
|
1140
|
+
TIME_HIGHRES: 'time:highres',
|
|
1141
|
+
|
|
1142
|
+
// IPC
|
|
1143
|
+
IPC_CHANNEL: 'ipc:channel',
|
|
1144
|
+
} as const;
|
|
1145
|
+
|
|
1146
|
+
export type WellKnownCapability = typeof Capabilities[keyof typeof Capabilities];
|