@korajs/auth 1.0.0-beta.12 → 1.0.0-beta.13

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 (101) hide show
  1. package/README.md +52 -47
  2. package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
  3. package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
  4. package/dist/index.cjs +645 -150
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +27 -9
  7. package/dist/index.d.ts +27 -9
  8. package/dist/index.js +644 -150
  9. package/dist/index.js.map +1 -1
  10. package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
  11. package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
  12. package/dist/react.d.cts +2 -2
  13. package/dist/react.d.ts +2 -2
  14. package/dist/server.cjs +2852 -1675
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +779 -168
  17. package/dist/server.d.ts +779 -168
  18. package/dist/server.js +2831 -1665
  19. package/dist/server.js.map +1 -1
  20. package/dist/svelte.cjs +2 -2
  21. package/dist/svelte.cjs.map +1 -1
  22. package/dist/svelte.d.cts +2 -2
  23. package/dist/svelte.d.ts +2 -2
  24. package/dist/svelte.js +2 -2
  25. package/dist/svelte.js.map +1 -1
  26. package/dist/vue.d.cts +1 -1
  27. package/dist/vue.d.ts +1 -1
  28. package/package.json +7 -7
  29. package/src/admin/admin-api.ts +327 -0
  30. package/src/admin/audit-log.ts +324 -0
  31. package/src/admin/webhooks.ts +576 -0
  32. package/src/bindings/create-auth-session.ts +184 -0
  33. package/src/bindings/create-org-session.ts +130 -0
  34. package/src/client/auth-client.ts +1592 -0
  35. package/src/client/auth-sync.ts +213 -0
  36. package/src/client/device-session.ts +104 -0
  37. package/src/client/org-client.ts +399 -0
  38. package/src/client/quickstart.ts +108 -0
  39. package/src/client/storage.ts +94 -0
  40. package/src/device/device-identity.ts +330 -0
  41. package/src/device/device-store.ts +379 -0
  42. package/src/encryption/auto-lock.ts +170 -0
  43. package/src/encryption/database-encryption.ts +265 -0
  44. package/src/encryption/key-derivation.ts +149 -0
  45. package/src/encryption/operation-encryptor.ts +361 -0
  46. package/src/index.ts +132 -0
  47. package/src/mfa/totp.ts +826 -0
  48. package/src/org/org-routes.ts +758 -0
  49. package/src/org/org-store.ts +490 -0
  50. package/src/org/org-types.ts +230 -0
  51. package/src/passkey/passkey-client.ts +597 -0
  52. package/src/passkey/passkey-server.ts +779 -0
  53. package/src/postgres/ensure-schema.ts +65 -0
  54. package/src/provider/adapter.ts +246 -0
  55. package/src/provider/built-in/auth-routes.ts +1313 -0
  56. package/src/provider/built-in/email-verification.ts +303 -0
  57. package/src/provider/built-in/password-hash.ts +118 -0
  58. package/src/provider/built-in/password-reset.ts +416 -0
  59. package/src/provider/built-in/postgres-user-store.ts +328 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +322 -0
  62. package/src/provider/built-in/sync-scopes.ts +85 -0
  63. package/src/provider/built-in/user-store.ts +465 -0
  64. package/src/provider/external/clerk-adapter.ts +157 -0
  65. package/src/provider/external/external-jwt-provider.ts +491 -0
  66. package/src/provider/external/supabase-adapter.ts +163 -0
  67. package/src/provider/oauth/linked-identity-store.ts +108 -0
  68. package/src/provider/oauth/oauth-flow.ts +550 -0
  69. package/src/provider/oauth/oauth-types.ts +184 -0
  70. package/src/provider/oauth/postgres-oauth-store.ts +296 -0
  71. package/src/provider/oauth/sqlite-oauth-store.ts +272 -0
  72. package/src/rbac/rbac-engine.ts +323 -0
  73. package/src/rbac/rbac-types.ts +210 -0
  74. package/src/rbac/scope-resolver.ts +140 -0
  75. package/src/react/AuthProvider.tsx +97 -0
  76. package/src/react/OrgProvider.tsx +41 -0
  77. package/src/react/auth-context.ts +26 -0
  78. package/src/react/hooks.ts +110 -0
  79. package/src/react/org-hooks.ts +214 -0
  80. package/src/react.ts +26 -0
  81. package/src/server.ts +334 -0
  82. package/src/session/session.ts +401 -0
  83. package/src/svelte/auth-context.ts +50 -0
  84. package/src/svelte/org-context.ts +32 -0
  85. package/src/svelte/org-hooks.ts +201 -0
  86. package/src/svelte/use-auth.ts +115 -0
  87. package/src/svelte.ts +25 -0
  88. package/src/tokens/encrypted-token-store.ts +360 -0
  89. package/src/tokens/jwt.ts +236 -0
  90. package/src/tokens/postgres-token-revocation-store.ts +140 -0
  91. package/src/tokens/sqlite-token-revocation-store.ts +121 -0
  92. package/src/tokens/token-manager.ts +821 -0
  93. package/src/tokens/token-store.ts +192 -0
  94. package/src/types.ts +394 -0
  95. package/src/vue/auth-context.ts +10 -0
  96. package/src/vue/auth-provider-types.ts +5 -0
  97. package/src/vue/auth-provider.ts +76 -0
  98. package/src/vue/org-hooks.ts +193 -0
  99. package/src/vue/org-provider.ts +49 -0
  100. package/src/vue/use-auth.ts +139 -0
  101. package/src/vue.ts +10 -0
@@ -0,0 +1,379 @@
1
+ import { KoraError } from '@korajs/core'
2
+
3
+ // --- Device key store errors ---
4
+
5
+ /**
6
+ * Thrown when a device key store operation fails.
7
+ * Provides context about the operation and the underlying cause.
8
+ */
9
+ export class DeviceKeyStoreError extends KoraError {
10
+ constructor(message: string, context?: Record<string, unknown>) {
11
+ super(message, 'DEVICE_KEY_STORE_ERROR', context)
12
+ this.name = 'DeviceKeyStoreError'
13
+ }
14
+ }
15
+
16
+ // --- Interface ---
17
+
18
+ /**
19
+ * Persistent storage interface for ECDSA P-256 device key pairs.
20
+ *
21
+ * Device key pairs are used for proof-of-possession authentication:
22
+ * the private key never leaves the device, and the public key is
23
+ * registered with the server. This store persists key pairs across
24
+ * page reloads and app restarts.
25
+ *
26
+ * In the browser, CryptoKey objects are structured-cloneable, so they
27
+ * can be stored directly in IndexedDB without serialization. In Node.js
28
+ * or test environments, an in-memory implementation is used instead.
29
+ */
30
+ export interface DeviceKeyStore {
31
+ /**
32
+ * Persist a key pair for the given device.
33
+ *
34
+ * Overwrites any previously stored key pair for the same device ID.
35
+ *
36
+ * @param deviceId - The unique device identifier (typically a UUID v7)
37
+ * @param keyPair - The ECDSA P-256 CryptoKeyPair to store
38
+ * @throws {DeviceKeyStoreError} If the storage operation fails
39
+ */
40
+ saveKeyPair(deviceId: string, keyPair: CryptoKeyPair): Promise<void>
41
+
42
+ /**
43
+ * Load a previously stored key pair for the given device.
44
+ *
45
+ * @param deviceId - The unique device identifier
46
+ * @returns The stored CryptoKeyPair, or null if no key pair exists for the device
47
+ * @throws {DeviceKeyStoreError} If the storage operation fails
48
+ */
49
+ loadKeyPair(deviceId: string): Promise<CryptoKeyPair | null>
50
+
51
+ /**
52
+ * Delete a stored key pair for the given device.
53
+ *
54
+ * No-op if no key pair exists for the device ID.
55
+ *
56
+ * @param deviceId - The unique device identifier
57
+ * @throws {DeviceKeyStoreError} If the storage operation fails
58
+ */
59
+ deleteKeyPair(deviceId: string): Promise<void>
60
+
61
+ /**
62
+ * Check whether a key pair exists for the given device.
63
+ *
64
+ * @param deviceId - The unique device identifier
65
+ * @returns True if a key pair is stored for the device, false otherwise
66
+ * @throws {DeviceKeyStoreError} If the storage operation fails
67
+ */
68
+ hasKeyPair(deviceId: string): Promise<boolean>
69
+ }
70
+
71
+ // --- IndexedDB constants ---
72
+
73
+ /** IndexedDB database name for device key storage. */
74
+ const IDB_DATABASE_NAME = 'kora_device_keys'
75
+
76
+ /** IndexedDB object store name within the database. */
77
+ const IDB_STORE_NAME = 'keypairs'
78
+
79
+ /** Current database schema version. */
80
+ const IDB_VERSION = 1
81
+
82
+ // --- IndexedDB implementation ---
83
+
84
+ /**
85
+ * Browser-based device key store backed by IndexedDB.
86
+ *
87
+ * CryptoKey objects are structured-cloneable, so they can be stored
88
+ * directly in IndexedDB without needing to export/import them as JWK.
89
+ * This preserves the non-extractable flag on private keys, ensuring
90
+ * they cannot be read even from storage.
91
+ *
92
+ * The database uses a single object store (`keypairs`) with the device ID
93
+ * as the key and the full CryptoKeyPair as the value.
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const store = new IndexedDBDeviceKeyStore()
98
+ * const keyPair = await generateDeviceKeyPair()
99
+ * await store.saveKeyPair('device-123', keyPair)
100
+ *
101
+ * const loaded = await store.loadKeyPair('device-123')
102
+ * // loaded.privateKey is still non-extractable
103
+ * ```
104
+ */
105
+ export class IndexedDBDeviceKeyStore implements DeviceKeyStore {
106
+ private dbPromise: Promise<IDBDatabase> | null = null
107
+
108
+ /**
109
+ * Opens (or creates) the IndexedDB database.
110
+ *
111
+ * The database connection is lazily initialized on first use and
112
+ * reused for subsequent operations. If the database does not exist,
113
+ * it is created with the `keypairs` object store.
114
+ */
115
+ private openDatabase(): Promise<IDBDatabase> {
116
+ // Reuse the database connection if already opened.
117
+ // This avoids repeatedly opening the database on every operation.
118
+ if (this.dbPromise !== null) {
119
+ return this.dbPromise
120
+ }
121
+
122
+ this.dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
123
+ let request: IDBOpenDBRequest
124
+
125
+ try {
126
+ request = globalThis.indexedDB.open(IDB_DATABASE_NAME, IDB_VERSION)
127
+ } catch (cause) {
128
+ this.dbPromise = null
129
+ reject(
130
+ new DeviceKeyStoreError(
131
+ 'Failed to open IndexedDB database for device key storage. ' +
132
+ 'IndexedDB may be unavailable or access may be denied.',
133
+ { cause: cause instanceof Error ? cause.message : String(cause) },
134
+ ),
135
+ )
136
+ return
137
+ }
138
+
139
+ request.onupgradeneeded = () => {
140
+ const db = request.result
141
+ if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
142
+ db.createObjectStore(IDB_STORE_NAME)
143
+ }
144
+ }
145
+
146
+ request.onsuccess = () => {
147
+ resolve(request.result)
148
+ }
149
+
150
+ request.onerror = () => {
151
+ this.dbPromise = null
152
+ reject(
153
+ new DeviceKeyStoreError('Failed to open IndexedDB database for device key storage.', {
154
+ error: request.error?.message,
155
+ }),
156
+ )
157
+ }
158
+
159
+ request.onblocked = () => {
160
+ this.dbPromise = null
161
+ reject(
162
+ new DeviceKeyStoreError(
163
+ 'IndexedDB database open was blocked. ' +
164
+ 'Another tab may have an older version of the database open. ' +
165
+ 'Close other tabs and try again.',
166
+ ),
167
+ )
168
+ }
169
+ })
170
+
171
+ return this.dbPromise
172
+ }
173
+
174
+ /** @inheritdoc */
175
+ async saveKeyPair(deviceId: string, keyPair: CryptoKeyPair): Promise<void> {
176
+ const db = await this.openDatabase()
177
+
178
+ return new Promise<void>((resolve, reject) => {
179
+ try {
180
+ const tx = db.transaction(IDB_STORE_NAME, 'readwrite')
181
+ const store = tx.objectStore(IDB_STORE_NAME)
182
+
183
+ // Store the CryptoKeyPair as a structured clone with the deviceId as the key.
184
+ // IndexedDB handles structured cloning of CryptoKey objects natively.
185
+ store.put(keyPair, deviceId)
186
+
187
+ tx.oncomplete = () => {
188
+ resolve()
189
+ }
190
+
191
+ tx.onerror = () => {
192
+ reject(
193
+ new DeviceKeyStoreError(`Failed to save key pair for device "${deviceId}".`, {
194
+ deviceId,
195
+ error: tx.error?.message,
196
+ }),
197
+ )
198
+ }
199
+ } catch (cause) {
200
+ reject(
201
+ new DeviceKeyStoreError(`Failed to save key pair for device "${deviceId}".`, {
202
+ deviceId,
203
+ cause: cause instanceof Error ? cause.message : String(cause),
204
+ }),
205
+ )
206
+ }
207
+ })
208
+ }
209
+
210
+ /** @inheritdoc */
211
+ async loadKeyPair(deviceId: string): Promise<CryptoKeyPair | null> {
212
+ const db = await this.openDatabase()
213
+
214
+ return new Promise<CryptoKeyPair | null>((resolve, reject) => {
215
+ try {
216
+ const tx = db.transaction(IDB_STORE_NAME, 'readonly')
217
+ const store = tx.objectStore(IDB_STORE_NAME)
218
+ const request = store.get(deviceId)
219
+
220
+ request.onsuccess = () => {
221
+ const result = request.result as CryptoKeyPair | undefined
222
+ resolve(result ?? null)
223
+ }
224
+
225
+ request.onerror = () => {
226
+ reject(
227
+ new DeviceKeyStoreError(`Failed to load key pair for device "${deviceId}".`, {
228
+ deviceId,
229
+ error: request.error?.message,
230
+ }),
231
+ )
232
+ }
233
+ } catch (cause) {
234
+ reject(
235
+ new DeviceKeyStoreError(`Failed to load key pair for device "${deviceId}".`, {
236
+ deviceId,
237
+ cause: cause instanceof Error ? cause.message : String(cause),
238
+ }),
239
+ )
240
+ }
241
+ })
242
+ }
243
+
244
+ /** @inheritdoc */
245
+ async deleteKeyPair(deviceId: string): Promise<void> {
246
+ const db = await this.openDatabase()
247
+
248
+ return new Promise<void>((resolve, reject) => {
249
+ try {
250
+ const tx = db.transaction(IDB_STORE_NAME, 'readwrite')
251
+ const store = tx.objectStore(IDB_STORE_NAME)
252
+ store.delete(deviceId)
253
+
254
+ tx.oncomplete = () => {
255
+ resolve()
256
+ }
257
+
258
+ tx.onerror = () => {
259
+ reject(
260
+ new DeviceKeyStoreError(`Failed to delete key pair for device "${deviceId}".`, {
261
+ deviceId,
262
+ error: tx.error?.message,
263
+ }),
264
+ )
265
+ }
266
+ } catch (cause) {
267
+ reject(
268
+ new DeviceKeyStoreError(`Failed to delete key pair for device "${deviceId}".`, {
269
+ deviceId,
270
+ cause: cause instanceof Error ? cause.message : String(cause),
271
+ }),
272
+ )
273
+ }
274
+ })
275
+ }
276
+
277
+ /** @inheritdoc */
278
+ async hasKeyPair(deviceId: string): Promise<boolean> {
279
+ const db = await this.openDatabase()
280
+
281
+ return new Promise<boolean>((resolve, reject) => {
282
+ try {
283
+ const tx = db.transaction(IDB_STORE_NAME, 'readonly')
284
+ const store = tx.objectStore(IDB_STORE_NAME)
285
+
286
+ // Use count() with the key to check existence without loading the value.
287
+ // This is more efficient than get() for large CryptoKeyPair objects.
288
+ const request = store.count(deviceId)
289
+
290
+ request.onsuccess = () => {
291
+ resolve(request.result > 0)
292
+ }
293
+
294
+ request.onerror = () => {
295
+ reject(
296
+ new DeviceKeyStoreError(
297
+ `Failed to check if key pair exists for device "${deviceId}".`,
298
+ { deviceId, error: request.error?.message },
299
+ ),
300
+ )
301
+ }
302
+ } catch (cause) {
303
+ reject(
304
+ new DeviceKeyStoreError(`Failed to check if key pair exists for device "${deviceId}".`, {
305
+ deviceId,
306
+ cause: cause instanceof Error ? cause.message : String(cause),
307
+ }),
308
+ )
309
+ }
310
+ })
311
+ }
312
+ }
313
+
314
+ // --- In-memory implementation ---
315
+
316
+ /**
317
+ * In-memory device key store for Node.js and testing environments.
318
+ *
319
+ * Key pairs are stored in a plain Map and do not survive process restarts.
320
+ * This is suitable for server-side rendering, Node.js scripts, and unit tests
321
+ * where IndexedDB is not available.
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * const store = new InMemoryDeviceKeyStore()
326
+ * const keyPair = await generateDeviceKeyPair()
327
+ * await store.saveKeyPair('device-123', keyPair)
328
+ *
329
+ * const loaded = await store.loadKeyPair('device-123')
330
+ * ```
331
+ */
332
+ export class InMemoryDeviceKeyStore implements DeviceKeyStore {
333
+ private readonly store = new Map<string, CryptoKeyPair>()
334
+
335
+ /** @inheritdoc */
336
+ async saveKeyPair(deviceId: string, keyPair: CryptoKeyPair): Promise<void> {
337
+ this.store.set(deviceId, keyPair)
338
+ }
339
+
340
+ /** @inheritdoc */
341
+ async loadKeyPair(deviceId: string): Promise<CryptoKeyPair | null> {
342
+ return this.store.get(deviceId) ?? null
343
+ }
344
+
345
+ /** @inheritdoc */
346
+ async deleteKeyPair(deviceId: string): Promise<void> {
347
+ this.store.delete(deviceId)
348
+ }
349
+
350
+ /** @inheritdoc */
351
+ async hasKeyPair(deviceId: string): Promise<boolean> {
352
+ return this.store.has(deviceId)
353
+ }
354
+ }
355
+
356
+ // --- Factory ---
357
+
358
+ /**
359
+ * Creates a DeviceKeyStore appropriate for the current environment.
360
+ *
361
+ * In browsers where IndexedDB is available, returns an {@link IndexedDBDeviceKeyStore}
362
+ * that persists CryptoKeyPair objects as structured clones. In Node.js, SSR,
363
+ * or environments without IndexedDB, returns an {@link InMemoryDeviceKeyStore}.
364
+ *
365
+ * @returns A DeviceKeyStore instance for the current environment
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * const store = createDeviceKeyStore()
370
+ * const keyPair = await generateDeviceKeyPair()
371
+ * await store.saveKeyPair(deviceId, keyPair)
372
+ * ```
373
+ */
374
+ export function createDeviceKeyStore(): DeviceKeyStore {
375
+ if (typeof globalThis.indexedDB !== 'undefined') {
376
+ return new IndexedDBDeviceKeyStore()
377
+ }
378
+ return new InMemoryDeviceKeyStore()
379
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Configuration for the AutoLockManager.
3
+ */
4
+ export interface AutoLockConfig {
5
+ /** Inactivity timeout in milliseconds before auto-locking. */
6
+ timeout: number
7
+ /** Callback invoked when the lock engages (either by timeout or manual lock). */
8
+ onLock: () => void
9
+ }
10
+
11
+ /**
12
+ * Manages inactivity-based auto-locking for the encrypted local store.
13
+ *
14
+ * The AutoLockManager starts an inactivity timer when {@link start} is called.
15
+ * If no user activity is reported via {@link reportActivity} within the configured
16
+ * timeout, the manager transitions to the locked state and invokes the `onLock`
17
+ * callback.
18
+ *
19
+ * This class has no DOM dependencies. It uses `setTimeout` and `clearTimeout` for
20
+ * timing and accepts an `onLock` callback for side effects. The consuming code is
21
+ * responsible for wiring DOM events (e.g., visibility changes, user interactions)
22
+ * to {@link reportActivity}.
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const manager = new AutoLockManager({
27
+ * timeout: 15 * 60 * 1000, // 15 minutes
28
+ * onLock: () => {
29
+ * // Clear decrypted data from memory
30
+ * // Show lock screen
31
+ * }
32
+ * })
33
+ *
34
+ * manager.start()
35
+ *
36
+ * // Call on user interactions to reset the timer
37
+ * document.addEventListener('click', () => manager.reportActivity())
38
+ * document.addEventListener('keydown', () => manager.reportActivity())
39
+ *
40
+ * // Check lock state
41
+ * if (manager.isLocked) {
42
+ * // Prompt for passphrase
43
+ * }
44
+ * ```
45
+ */
46
+ export class AutoLockManager {
47
+ private readonly _timeout: number
48
+ private readonly _onLock: () => void
49
+ private _timerId: ReturnType<typeof setTimeout> | null = null
50
+ private _isLocked = false
51
+ private _isRunning = false
52
+
53
+ constructor(config: AutoLockConfig) {
54
+ if (config.timeout <= 0) {
55
+ throw new Error(
56
+ `AutoLockManager timeout must be a positive number, but received ${config.timeout}.`,
57
+ )
58
+ }
59
+
60
+ this._timeout = config.timeout
61
+ this._onLock = config.onLock
62
+ }
63
+
64
+ /**
65
+ * Whether the manager is currently in the locked state.
66
+ *
67
+ * Becomes `true` when the inactivity timeout fires or {@link lock} is called manually.
68
+ * Returns to `false` only when {@link unlock} is called.
69
+ */
70
+ get isLocked(): boolean {
71
+ return this._isLocked
72
+ }
73
+
74
+ /**
75
+ * Starts monitoring for inactivity.
76
+ *
77
+ * Begins the inactivity countdown. If the manager is already running, this is
78
+ * a no-op to prevent creating multiple timers. Calling `start()` also resets
79
+ * the locked state if the manager was previously locked.
80
+ */
81
+ start(): void {
82
+ if (this._isRunning) {
83
+ return
84
+ }
85
+
86
+ this._isRunning = true
87
+ this._isLocked = false
88
+ this._startTimer()
89
+ }
90
+
91
+ /**
92
+ * Stops monitoring for inactivity.
93
+ *
94
+ * Clears the pending inactivity timer. Does not change the lock state: if
95
+ * the manager was locked, it remains locked. If unlocked, it remains unlocked.
96
+ * To resume monitoring, call {@link start} again.
97
+ */
98
+ stop(): void {
99
+ this._isRunning = false
100
+ this._clearTimer()
101
+ }
102
+
103
+ /**
104
+ * Reports user activity, resetting the inactivity timer.
105
+ *
106
+ * Call this whenever the user interacts with the application (clicks, key presses,
107
+ * touches, etc.). If the manager is not running or is already locked, this is a no-op.
108
+ */
109
+ reportActivity(): void {
110
+ if (!this._isRunning || this._isLocked) {
111
+ return
112
+ }
113
+
114
+ this._clearTimer()
115
+ this._startTimer()
116
+ }
117
+
118
+ /**
119
+ * Manually locks the manager immediately.
120
+ *
121
+ * Clears the inactivity timer and transitions to the locked state. The `onLock`
122
+ * callback is invoked. The manager remains running but locked: call {@link unlock}
123
+ * to return to the unlocked state, which will restart the inactivity timer.
124
+ */
125
+ lock(): void {
126
+ this._clearTimer()
127
+
128
+ if (!this._isLocked) {
129
+ this._isLocked = true
130
+ this._onLock()
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Unlocks the manager, returning to the unlocked state.
136
+ *
137
+ * If the manager is running, the inactivity timer is restarted. If the manager
138
+ * is not running (was stopped), it simply clears the locked state without
139
+ * starting a timer.
140
+ */
141
+ unlock(): void {
142
+ this._isLocked = false
143
+
144
+ if (this._isRunning) {
145
+ this._clearTimer()
146
+ this._startTimer()
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Starts the inactivity timer. When it fires, the manager locks.
152
+ */
153
+ private _startTimer(): void {
154
+ this._timerId = setTimeout(() => {
155
+ this._timerId = null
156
+ this._isLocked = true
157
+ this._onLock()
158
+ }, this._timeout)
159
+ }
160
+
161
+ /**
162
+ * Clears the pending inactivity timer, if any.
163
+ */
164
+ private _clearTimer(): void {
165
+ if (this._timerId !== null) {
166
+ clearTimeout(this._timerId)
167
+ this._timerId = null
168
+ }
169
+ }
170
+ }