react-native-nitro-storage 0.13.0 → 0.15.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/CHANGELOG.md +31 -0
- package/README.md +260 -7
- package/cpp/bindings/HybridStorage.cpp +21 -0
- package/cpp/bindings/HybridStorage.hpp +2 -0
- package/cpp/core/NativeStorageAdapter.hpp +6 -0
- package/docs/api-reference.md +68 -43
- package/docs/batch-transactions-migrations.md +7 -0
- package/docs/qa/agent-device-replay.md +106 -0
- package/docs/secure-storage.md +12 -6
- package/ios/IOSStorageAdapterCpp.hpp +12 -0
- package/ios/IOSStorageAdapterCpp.mm +180 -2
- package/lib/commonjs/Storage.types.js +4 -2
- package/lib/commonjs/Storage.types.js.map +1 -1
- package/lib/commonjs/core/durability.js +30 -2
- package/lib/commonjs/core/durability.js.map +1 -1
- package/lib/commonjs/index.js +16 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +10 -1
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/shared.js +6 -0
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +116 -3
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/testing.js +32 -2
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/Storage.types.js +4 -2
- package/lib/module/Storage.types.js.map +1 -1
- package/lib/module/core/durability.js +30 -2
- package/lib/module/core/durability.js.map +1 -1
- package/lib/module/index.js +16 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +10 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/shared.js +5 -0
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +117 -4
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/testing.js +31 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/Storage.nitro.d.ts +2 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/Storage.types.d.ts +4 -2
- package/lib/typescript/Storage.types.d.ts.map +1 -1
- package/lib/typescript/core/durability.d.ts +7 -1
- package/lib/typescript/core/durability.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +8 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +8 -3
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +9 -1
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +34 -4
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-platform.d.ts +2 -0
- package/lib/typescript/storage-platform.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +16 -5
- package/lib/typescript/testing.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
- package/package.json +2 -2
- package/src/Storage.nitro.ts +2 -0
- package/src/Storage.types.ts +4 -2
- package/src/core/durability.ts +46 -2
- package/src/index.ts +24 -0
- package/src/index.web.ts +18 -0
- package/src/shared.ts +27 -1
- package/src/storage-core.ts +182 -8
- package/src/storage-platform.ts +2 -0
- package/src/testing.ts +42 -0
package/src/Storage.nitro.ts
CHANGED
|
@@ -26,4 +26,6 @@ export interface Storage extends HybridObject<{ ios: "c++"; android: "c++" }> {
|
|
|
26
26
|
deleteSecureBiometric(key: string): void;
|
|
27
27
|
hasSecureBiometric(key: string): boolean;
|
|
28
28
|
clearSecureBiometric(): void;
|
|
29
|
+
isProtectedDataAvailable(): boolean;
|
|
30
|
+
onProtectedDataAvailable(listener: () => void): () => void;
|
|
29
31
|
}
|
package/src/Storage.types.ts
CHANGED
|
@@ -20,8 +20,10 @@ export enum AccessControl {
|
|
|
20
20
|
export enum BiometricLevel {
|
|
21
21
|
/** No biometric requirement (default). */
|
|
22
22
|
None = 0,
|
|
23
|
-
/**
|
|
23
|
+
/** iOS: Keychain user presence. Android: recent authentication when opening
|
|
24
|
+
* the protected store; later access uses its cached keyset. Web: no biometric enforcement. */
|
|
24
25
|
BiometryOrPasscode = 1,
|
|
25
|
-
/**
|
|
26
|
+
/** Biometric-only Keychain/Keystore policy, without passcode fallback.
|
|
27
|
+
* Android authenticates when opening the store, not each access. Web: no biometric enforcement. */
|
|
26
28
|
BiometryOnly = 2,
|
|
27
29
|
}
|
package/src/core/durability.ts
CHANGED
|
@@ -2,6 +2,15 @@ import { runMicrotask } from "../shared";
|
|
|
2
2
|
import { StorageScope, type AccessControl } from "../Storage.types";
|
|
3
3
|
import type { PendingDiskWrite, PendingSecureWrite } from "../shared";
|
|
4
4
|
|
|
5
|
+
export type StorageScheduledFlushError = {
|
|
6
|
+
readonly scope: StorageScope.Disk | StorageScope.Secure;
|
|
7
|
+
readonly error: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type StorageScheduledFlushErrorObserver = (
|
|
11
|
+
error: StorageScheduledFlushError,
|
|
12
|
+
) => void;
|
|
13
|
+
|
|
5
14
|
export type DurabilityBackend = {
|
|
6
15
|
setBatch(keys: string[], values: string[], scope: number): void;
|
|
7
16
|
removeBatch(keys: string[], scope: number): void;
|
|
@@ -9,6 +18,9 @@ export type DurabilityBackend = {
|
|
|
9
18
|
};
|
|
10
19
|
|
|
11
20
|
export type DurabilityCoordinator = {
|
|
21
|
+
setScheduledFlushErrorObserver(
|
|
22
|
+
observer: StorageScheduledFlushErrorObserver | undefined,
|
|
23
|
+
): void;
|
|
12
24
|
setDiskWritesAsync(enabled: boolean): void;
|
|
13
25
|
isDiskWritesAsync(): boolean;
|
|
14
26
|
hasPendingDiskWrite(key: string): boolean;
|
|
@@ -42,6 +54,8 @@ export function createDurabilityCoordinator(options: {
|
|
|
42
54
|
let nextGeneration = 0;
|
|
43
55
|
let diskFlushScheduled = false;
|
|
44
56
|
let diskWritesAsync = false;
|
|
57
|
+
let scheduledFlushErrorObserver:
|
|
58
|
+
StorageScheduledFlushErrorObserver | undefined;
|
|
45
59
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
46
60
|
let secureFlushScheduled = false;
|
|
47
61
|
const securePromotionsInProgress = new Set<string>();
|
|
@@ -168,6 +182,33 @@ export function createDurabilityCoordinator(options: {
|
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
|
|
185
|
+
function reportScheduledFlushError(
|
|
186
|
+
scope: StorageScheduledFlushError["scope"],
|
|
187
|
+
error: unknown,
|
|
188
|
+
): void {
|
|
189
|
+
const observer = scheduledFlushErrorObserver;
|
|
190
|
+
if (observer === undefined) {
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
observer({ scope, error });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function flushScheduledDiskWrites(): void {
|
|
197
|
+
try {
|
|
198
|
+
flushDiskWrites();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
reportScheduledFlushError(StorageScope.Disk, error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function flushScheduledSecureWrites(): void {
|
|
205
|
+
try {
|
|
206
|
+
flushSecureWrites();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
reportScheduledFlushError(StorageScope.Secure, error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
171
212
|
function scheduleDiskWrite(
|
|
172
213
|
key: string,
|
|
173
214
|
value: string | undefined,
|
|
@@ -182,7 +223,7 @@ export function createDurabilityCoordinator(options: {
|
|
|
182
223
|
return pendingWrite;
|
|
183
224
|
}
|
|
184
225
|
diskFlushScheduled = true;
|
|
185
|
-
runMicrotask(
|
|
226
|
+
runMicrotask(flushScheduledDiskWrites);
|
|
186
227
|
return pendingWrite;
|
|
187
228
|
}
|
|
188
229
|
|
|
@@ -204,7 +245,7 @@ export function createDurabilityCoordinator(options: {
|
|
|
204
245
|
return pendingWrite;
|
|
205
246
|
}
|
|
206
247
|
secureFlushScheduled = true;
|
|
207
|
-
runMicrotask(
|
|
248
|
+
runMicrotask(flushScheduledSecureWrites);
|
|
208
249
|
return pendingWrite;
|
|
209
250
|
}
|
|
210
251
|
|
|
@@ -225,6 +266,9 @@ export function createDurabilityCoordinator(options: {
|
|
|
225
266
|
}
|
|
226
267
|
|
|
227
268
|
return {
|
|
269
|
+
setScheduledFlushErrorObserver(observer) {
|
|
270
|
+
scheduledFlushErrorObserver = observer;
|
|
271
|
+
},
|
|
228
272
|
setDiskWritesAsync(enabled) {
|
|
229
273
|
diskWritesAsync = enabled;
|
|
230
274
|
if (!enabled) {
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { createIndexedDBBackend as createIndexedDBBackendFromSubpath } from "./i
|
|
|
8
8
|
import { unescapeCollidingRawValue } from "./internal";
|
|
9
9
|
import {
|
|
10
10
|
assertAccessControlLevel,
|
|
11
|
+
assertProtectedDataListener,
|
|
11
12
|
notifyAllListeners,
|
|
12
13
|
notifyKeyListeners,
|
|
13
14
|
type NonMemoryScope,
|
|
@@ -49,6 +50,10 @@ export type {
|
|
|
49
50
|
VersionedValue,
|
|
50
51
|
} from "./shared";
|
|
51
52
|
export { isKeychainLockedError } from "./shared";
|
|
53
|
+
export type {
|
|
54
|
+
StorageScheduledFlushError,
|
|
55
|
+
StorageScheduledFlushErrorObserver,
|
|
56
|
+
} from "./core/durability";
|
|
52
57
|
|
|
53
58
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
54
59
|
export type { Storage } from "./Storage.nitro";
|
|
@@ -78,6 +83,8 @@ export type {
|
|
|
78
83
|
WebStorageScope,
|
|
79
84
|
} from "./web-storage-backend";
|
|
80
85
|
export type {
|
|
86
|
+
SecureAccessControlMigrationOptions,
|
|
87
|
+
SecureAccessControlMigrationResult,
|
|
81
88
|
SetItemConfig,
|
|
82
89
|
SetStorageItem,
|
|
83
90
|
StorageBatchSetItem,
|
|
@@ -260,6 +267,7 @@ function buildNativeAdapter(
|
|
|
260
267
|
backend: nativeBackend,
|
|
261
268
|
changeSource: "native",
|
|
262
269
|
applyAccessControlOnSecureRawWrite: true,
|
|
270
|
+
supportsSecureAccessControlMigration: () => Platform.OS === "ios",
|
|
263
271
|
ensureScopeSubscription: ensureNativeScopeSubscription,
|
|
264
272
|
maybeCleanupScopeSubscription: maybeCleanupNativeScopeSubscription,
|
|
265
273
|
onWillEmitChanges: (scope, keys, operation, source) => {
|
|
@@ -300,6 +308,21 @@ export const storage = {
|
|
|
300
308
|
},
|
|
301
309
|
);
|
|
302
310
|
},
|
|
311
|
+
isProtectedDataAvailable: (): boolean => {
|
|
312
|
+
const module = getStorageModule();
|
|
313
|
+
return typeof module.isProtectedDataAvailable === "function"
|
|
314
|
+
? module.isProtectedDataAvailable()
|
|
315
|
+
: true;
|
|
316
|
+
},
|
|
317
|
+
onProtectedDataAvailable: (listener: () => void): (() => void) => {
|
|
318
|
+
assertProtectedDataListener(listener);
|
|
319
|
+
const module = getStorageModule();
|
|
320
|
+
if (typeof module.onProtectedDataAvailable !== "function") {
|
|
321
|
+
return () => {};
|
|
322
|
+
}
|
|
323
|
+
const unsubscribe = module.onProtectedDataAvailable(listener);
|
|
324
|
+
return typeof unsubscribe === "function" ? unsubscribe : () => {};
|
|
325
|
+
},
|
|
303
326
|
setSecureWritesAsync: (enabled: boolean) => {
|
|
304
327
|
internals.measureOperation(
|
|
305
328
|
"storage:setSecureWritesAsync",
|
|
@@ -364,6 +387,7 @@ export const setBatch = core.setBatch;
|
|
|
364
387
|
export const removeBatch = core.removeBatch;
|
|
365
388
|
export const registerMigration = core.registerMigration;
|
|
366
389
|
export const migrateToLatest = core.migrateToLatest;
|
|
390
|
+
export const migrateSecureAccessControl = core.migrateSecureAccessControl;
|
|
367
391
|
export const runTransaction = core.runTransaction;
|
|
368
392
|
export const createSecureAuthStorage = core.createSecureAuthStorage;
|
|
369
393
|
|
package/src/index.web.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createIndexedDBBackend as createIndexedDBBackendFromSubpath } from "./i
|
|
|
3
3
|
import { unescapeCollidingRawValue } from "./internal";
|
|
4
4
|
import {
|
|
5
5
|
assertAccessControlLevel,
|
|
6
|
+
assertProtectedDataListener,
|
|
6
7
|
createStorageCompositeError,
|
|
7
8
|
assertBiometricLevel,
|
|
8
9
|
notifyAllListeners,
|
|
@@ -48,6 +49,10 @@ export type {
|
|
|
48
49
|
VersionedValue,
|
|
49
50
|
} from "./shared";
|
|
50
51
|
export { isKeychainLockedError } from "./shared";
|
|
52
|
+
export type {
|
|
53
|
+
StorageScheduledFlushError,
|
|
54
|
+
StorageScheduledFlushErrorObserver,
|
|
55
|
+
} from "./core/durability";
|
|
51
56
|
|
|
52
57
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
53
58
|
export type { Storage } from "./Storage.nitro";
|
|
@@ -82,6 +87,8 @@ export {
|
|
|
82
87
|
type WebBackendCapabilities,
|
|
83
88
|
} from "./web-backend-contract";
|
|
84
89
|
export type {
|
|
90
|
+
SecureAccessControlMigrationOptions,
|
|
91
|
+
SecureAccessControlMigrationResult,
|
|
85
92
|
SetItemConfig,
|
|
86
93
|
SetStorageItem,
|
|
87
94
|
StorageBatchSetItem,
|
|
@@ -807,6 +814,11 @@ const WebStorage: Storage = {
|
|
|
807
814
|
},
|
|
808
815
|
setSecureWritesAsync: (_enabled: boolean) => {},
|
|
809
816
|
setKeychainAccessGroup: () => {},
|
|
817
|
+
isProtectedDataAvailable: () => true,
|
|
818
|
+
onProtectedDataAvailable: (listener: () => void) => {
|
|
819
|
+
assertProtectedDataListener(listener);
|
|
820
|
+
return () => {};
|
|
821
|
+
},
|
|
810
822
|
setSecureBiometric: (key: string, value: string) => {
|
|
811
823
|
WebStorage.setSecureBiometricWithLevel(
|
|
812
824
|
key,
|
|
@@ -1019,6 +1031,7 @@ function buildWebAdapter(
|
|
|
1019
1031
|
backend: WebStorage,
|
|
1020
1032
|
changeSource: "web",
|
|
1021
1033
|
applyAccessControlOnSecureRawWrite: false,
|
|
1034
|
+
supportsSecureAccessControlMigration: () => false,
|
|
1022
1035
|
ensureScopeSubscription: () => {
|
|
1023
1036
|
ensureExternalSyncSubscriptions();
|
|
1024
1037
|
},
|
|
@@ -1045,6 +1058,10 @@ export const storage = {
|
|
|
1045
1058
|
0,
|
|
1046
1059
|
);
|
|
1047
1060
|
},
|
|
1061
|
+
isProtectedDataAvailable: (): boolean =>
|
|
1062
|
+
WebStorage.isProtectedDataAvailable(),
|
|
1063
|
+
onProtectedDataAvailable: (listener: () => void): (() => void) =>
|
|
1064
|
+
WebStorage.onProtectedDataAvailable(listener),
|
|
1048
1065
|
setSecureWritesAsync: (_enabled: boolean) => {
|
|
1049
1066
|
getInternals().recordMetric(
|
|
1050
1067
|
"storage:setSecureWritesAsync",
|
|
@@ -1110,6 +1127,7 @@ export const setBatch = core.setBatch;
|
|
|
1110
1127
|
export const removeBatch = core.removeBatch;
|
|
1111
1128
|
export const registerMigration = core.registerMigration;
|
|
1112
1129
|
export const migrateToLatest = core.migrateToLatest;
|
|
1130
|
+
export const migrateSecureAccessControl = core.migrateSecureAccessControl;
|
|
1113
1131
|
export const runTransaction = core.runTransaction;
|
|
1114
1132
|
export const createSecureAuthStorage = core.createSecureAuthStorage;
|
|
1115
1133
|
|
package/src/shared.ts
CHANGED
|
@@ -75,7 +75,23 @@ export type MigrationContext = {
|
|
|
75
75
|
removeRaw: (key: string) => void;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
type CallableThenableResult<TResult> = TResult extends {
|
|
79
|
+
then?: infer Then;
|
|
80
|
+
}
|
|
81
|
+
? Extract<Then, (...args: never[]) => unknown> extends never
|
|
82
|
+
? never
|
|
83
|
+
: TResult
|
|
84
|
+
: never;
|
|
85
|
+
|
|
86
|
+
export type SynchronousCallbackResult<TResult> = [
|
|
87
|
+
CallableThenableResult<TResult>,
|
|
88
|
+
] extends [never]
|
|
89
|
+
? TResult
|
|
90
|
+
: never;
|
|
91
|
+
|
|
92
|
+
export type Migration<TResult = unknown> = (
|
|
93
|
+
context: MigrationContext,
|
|
94
|
+
) => SynchronousCallbackResult<TResult>;
|
|
79
95
|
|
|
80
96
|
export type KeyListenerRegistry = Map<string, Set<() => void>>;
|
|
81
97
|
|
|
@@ -212,6 +228,16 @@ export function assertBiometricLevel(level: number): void {
|
|
|
212
228
|
assertEnumInteger(level, 0, 2, "biometric level");
|
|
213
229
|
}
|
|
214
230
|
|
|
231
|
+
export function assertProtectedDataListener(
|
|
232
|
+
listener: unknown,
|
|
233
|
+
): asserts listener is () => void {
|
|
234
|
+
if (typeof listener !== "function") {
|
|
235
|
+
throw new TypeError(
|
|
236
|
+
"NitroStorage: Protected data listener must be a function",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
215
241
|
export type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
216
242
|
|
|
217
243
|
export type PendingDiskWrite = {
|
package/src/storage-core.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createDurabilityCoordinator,
|
|
3
|
+
type StorageScheduledFlushErrorObserver,
|
|
4
|
+
} from "./core/durability";
|
|
2
5
|
import { createMetricsRegistry } from "./core/metrics";
|
|
3
6
|
import {
|
|
4
7
|
MIGRATION_VERSION_KEY,
|
|
@@ -44,6 +47,7 @@ import {
|
|
|
44
47
|
type StorageMetricsObserver,
|
|
45
48
|
type StorageSelectorListener,
|
|
46
49
|
type StorageSelectorSubscribeOptions,
|
|
50
|
+
type SynchronousCallbackResult,
|
|
47
51
|
type StorageVersion,
|
|
48
52
|
type Validator,
|
|
49
53
|
type VersionedValue,
|
|
@@ -58,7 +62,12 @@ import {
|
|
|
58
62
|
type StorageKeyChangeEvent,
|
|
59
63
|
} from "./storage-events";
|
|
60
64
|
import type { StorageSetter } from "./storage-hooks";
|
|
61
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
getStorageErrorCode,
|
|
67
|
+
isStorageError,
|
|
68
|
+
type SecureStorageMetadata,
|
|
69
|
+
type StorageErrorCode,
|
|
70
|
+
} from "./storage-runtime";
|
|
62
71
|
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
63
72
|
|
|
64
73
|
export type TransactionContext = {
|
|
@@ -76,6 +85,28 @@ export type TransactionContext = {
|
|
|
76
85
|
) => void;
|
|
77
86
|
};
|
|
78
87
|
|
|
88
|
+
function assertSynchronousCallbackResult(
|
|
89
|
+
value: unknown,
|
|
90
|
+
callbackKind: "Transaction" | "Migration",
|
|
91
|
+
): void {
|
|
92
|
+
if (
|
|
93
|
+
value === null ||
|
|
94
|
+
(typeof value !== "object" && typeof value !== "function")
|
|
95
|
+
) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const then = (value as { then?: unknown }).then;
|
|
100
|
+
if (typeof then !== "function") {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
void Promise.resolve(value).catch(() => undefined);
|
|
105
|
+
throw new TypeError(
|
|
106
|
+
`${callbackKind} callbacks must be synchronous and cannot return a Promise or thenable.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
79
110
|
export type StorageItemConfig<T> = {
|
|
80
111
|
key: string;
|
|
81
112
|
scope: StorageScope;
|
|
@@ -191,6 +222,30 @@ export type StorageClearOptions = {
|
|
|
191
222
|
except?: readonly StorageKeyRef[];
|
|
192
223
|
};
|
|
193
224
|
|
|
225
|
+
export type SecureAccessControlMigrationOptions = {
|
|
226
|
+
/** Secure keys to rewrite. Defaults to every Secure key. */
|
|
227
|
+
keys?: readonly string[];
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export type SecureAccessControlMigrationResult = {
|
|
231
|
+
/** Keys rewritten so the Keychain item carries the new access level. */
|
|
232
|
+
migrated: string[];
|
|
233
|
+
/** Keys left unchanged because the Keychain was locked (`keychain_locked`). */
|
|
234
|
+
locked: string[];
|
|
235
|
+
/** Keys that no longer exist when the migration reached them. */
|
|
236
|
+
missing: string[];
|
|
237
|
+
/** Biometric-protected keys, and every key on platforms without access classes. */
|
|
238
|
+
skipped: string[];
|
|
239
|
+
/** Keys left unchanged because of another error. */
|
|
240
|
+
failed: { key: string; code?: StorageErrorCode }[];
|
|
241
|
+
/**
|
|
242
|
+
* `true` when flushing queued Secure writes or listing the Secure keys
|
|
243
|
+
* failed with `keychain_locked`. The default level and every item are
|
|
244
|
+
* unchanged; retry once protected data is available.
|
|
245
|
+
*/
|
|
246
|
+
enumerationLocked: boolean;
|
|
247
|
+
};
|
|
248
|
+
|
|
194
249
|
export type SetItemConfig<TMember extends string = string> = Omit<
|
|
195
250
|
StorageItemConfig<Record<string, true>>,
|
|
196
251
|
"defaultValue" | "serialize" | "deserialize"
|
|
@@ -248,6 +303,7 @@ export type StorageCoreAdapter = {
|
|
|
248
303
|
backend: StorageCoreBackend;
|
|
249
304
|
changeSource: StorageChangeSource;
|
|
250
305
|
applyAccessControlOnSecureRawWrite: boolean;
|
|
306
|
+
supportsSecureAccessControlMigration(): boolean;
|
|
251
307
|
ensureScopeSubscription(scope: NonMemoryScope): void;
|
|
252
308
|
maybeCleanupScopeSubscription(scope: NonMemoryScope): void;
|
|
253
309
|
onWillEmitChanges(
|
|
@@ -318,7 +374,7 @@ function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
|
|
|
318
374
|
export function createStorageCore(
|
|
319
375
|
buildAdapter: (internals: StorageCoreInternals) => StorageCoreAdapter,
|
|
320
376
|
) {
|
|
321
|
-
const registeredMigrations = new Map<number, Migration
|
|
377
|
+
const registeredMigrations = new Map<number, Migration<unknown>>();
|
|
322
378
|
const itemGroups = new Map<string, Set<StorageItemInternal<unknown>>>();
|
|
323
379
|
const registeredKeyCounts = new Map<string, number>();
|
|
324
380
|
const memoryStore = new Map<string, unknown>();
|
|
@@ -1485,6 +1541,12 @@ export function createStorageCore(
|
|
|
1485
1541
|
flushSecureWrites();
|
|
1486
1542
|
});
|
|
1487
1543
|
},
|
|
1544
|
+
/** Observes scheduled flush failures; explicit flush calls still throw. */
|
|
1545
|
+
setScheduledFlushErrorObserver: (
|
|
1546
|
+
observer?: StorageScheduledFlushErrorObserver,
|
|
1547
|
+
): void => {
|
|
1548
|
+
durability.setScheduledFlushErrorObserver(observer);
|
|
1549
|
+
},
|
|
1488
1550
|
setMetricsObserver: (observer?: StorageMetricsObserver) => {
|
|
1489
1551
|
metrics.setObserver(observer);
|
|
1490
1552
|
},
|
|
@@ -3320,7 +3382,10 @@ export function createStorageCore(
|
|
|
3320
3382
|
);
|
|
3321
3383
|
}
|
|
3322
3384
|
|
|
3323
|
-
function registerMigration(
|
|
3385
|
+
function registerMigration<TResult>(
|
|
3386
|
+
version: number,
|
|
3387
|
+
migration: Migration<TResult>,
|
|
3388
|
+
): void {
|
|
3324
3389
|
if (!Number.isInteger(version) || version <= 0) {
|
|
3325
3390
|
throw new Error("Migration version must be a positive integer.");
|
|
3326
3391
|
}
|
|
@@ -3332,6 +3397,95 @@ export function createStorageCore(
|
|
|
3332
3397
|
registeredMigrations.set(version, migration);
|
|
3333
3398
|
}
|
|
3334
3399
|
|
|
3400
|
+
function migrateSecureAccessControl(
|
|
3401
|
+
level: AccessControl,
|
|
3402
|
+
options?: SecureAccessControlMigrationOptions,
|
|
3403
|
+
): SecureAccessControlMigrationResult {
|
|
3404
|
+
return measureOperation(
|
|
3405
|
+
"storage:migrateSecureAccessControl",
|
|
3406
|
+
StorageScope.Secure,
|
|
3407
|
+
() => {
|
|
3408
|
+
assertAccessControlLevel(level);
|
|
3409
|
+
const requestedKeys = options?.keys;
|
|
3410
|
+
requestedKeys?.forEach((key) => {
|
|
3411
|
+
assertValidStorageKey(key);
|
|
3412
|
+
});
|
|
3413
|
+
|
|
3414
|
+
let keys: string[];
|
|
3415
|
+
try {
|
|
3416
|
+
flushSecureWrites();
|
|
3417
|
+
keys = Array.from(
|
|
3418
|
+
new Set(
|
|
3419
|
+
requestedKeys ?? adapter.backend.getAllKeys(StorageScope.Secure),
|
|
3420
|
+
),
|
|
3421
|
+
);
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
if (getStorageErrorCode(error) !== "keychain_locked") {
|
|
3424
|
+
throw error;
|
|
3425
|
+
}
|
|
3426
|
+
return {
|
|
3427
|
+
migrated: [],
|
|
3428
|
+
locked: [],
|
|
3429
|
+
missing: [],
|
|
3430
|
+
skipped: [],
|
|
3431
|
+
failed: [],
|
|
3432
|
+
enumerationLocked: true,
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
secureDefaultAccessControl = level;
|
|
3436
|
+
adapter.backend.setSecureAccessControl(level);
|
|
3437
|
+
|
|
3438
|
+
const result: SecureAccessControlMigrationResult = {
|
|
3439
|
+
migrated: [],
|
|
3440
|
+
locked: [],
|
|
3441
|
+
missing: [],
|
|
3442
|
+
skipped: [],
|
|
3443
|
+
failed: [],
|
|
3444
|
+
enumerationLocked: false,
|
|
3445
|
+
};
|
|
3446
|
+
if (!adapter.supportsSecureAccessControlMigration()) {
|
|
3447
|
+
result.skipped = keys;
|
|
3448
|
+
return result;
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
keys.forEach((key) => {
|
|
3452
|
+
try {
|
|
3453
|
+
let hasBiometric: boolean;
|
|
3454
|
+
try {
|
|
3455
|
+
hasBiometric = adapter.backend.hasSecureBiometric(key);
|
|
3456
|
+
} catch (error) {
|
|
3457
|
+
if (getStorageErrorCode(error) !== "keychain_locked") {
|
|
3458
|
+
throw error;
|
|
3459
|
+
}
|
|
3460
|
+
result.skipped.push(key);
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
if (hasBiometric) {
|
|
3464
|
+
result.skipped.push(key);
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
const raw = adapter.backend.get(key, StorageScope.Secure);
|
|
3468
|
+
if (raw === undefined) {
|
|
3469
|
+
result.missing.push(key);
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
adapter.backend.set(key, raw, StorageScope.Secure);
|
|
3473
|
+
result.migrated.push(key);
|
|
3474
|
+
} catch (error) {
|
|
3475
|
+
const code = getStorageErrorCode(error);
|
|
3476
|
+
if (code === "keychain_locked") {
|
|
3477
|
+
result.locked.push(key);
|
|
3478
|
+
} else {
|
|
3479
|
+
result.failed.push(code === undefined ? { key } : { key, code });
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
});
|
|
3483
|
+
return result;
|
|
3484
|
+
},
|
|
3485
|
+
options?.keys?.length ?? 1,
|
|
3486
|
+
);
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3335
3489
|
function migrateToLatest(scope: StorageScope = StorageScope.Disk): number {
|
|
3336
3490
|
return measureOperation("migration:run", scope, () => {
|
|
3337
3491
|
assertValidScope(scope);
|
|
@@ -3348,7 +3502,8 @@ export function createStorageCore(
|
|
|
3348
3502
|
return;
|
|
3349
3503
|
}
|
|
3350
3504
|
runTransaction(scope, (tx) => {
|
|
3351
|
-
migration(tx);
|
|
3505
|
+
const migrationResult: unknown = migration(tx);
|
|
3506
|
+
assertSynchronousCallbackResult(migrationResult, "Migration");
|
|
3352
3507
|
tx.setRaw(MIGRATION_VERSION_KEY, String(version));
|
|
3353
3508
|
});
|
|
3354
3509
|
appliedVersion = version;
|
|
@@ -3360,7 +3515,7 @@ export function createStorageCore(
|
|
|
3360
3515
|
|
|
3361
3516
|
function runTransaction<T>(
|
|
3362
3517
|
scope: StorageScope,
|
|
3363
|
-
transaction: (context: TransactionContext) => T
|
|
3518
|
+
transaction: (context: TransactionContext) => SynchronousCallbackResult<T>,
|
|
3364
3519
|
): T {
|
|
3365
3520
|
return measureOperation("transaction:run", scope, () => {
|
|
3366
3521
|
assertValidScope(scope);
|
|
@@ -3379,6 +3534,12 @@ export function createStorageCore(
|
|
|
3379
3534
|
};
|
|
3380
3535
|
const rollback = new Map<string, TransactionRollbackEntry>();
|
|
3381
3536
|
const itemRenameStates = new Map<StorageItemInternal<unknown>, boolean>();
|
|
3537
|
+
let transactionOpen = true;
|
|
3538
|
+
const assertTransactionOpen = (): void => {
|
|
3539
|
+
if (!transactionOpen) {
|
|
3540
|
+
throw new TypeError("Transaction context is closed.");
|
|
3541
|
+
}
|
|
3542
|
+
};
|
|
3382
3543
|
|
|
3383
3544
|
const rememberRollback = (
|
|
3384
3545
|
key: string,
|
|
@@ -3486,26 +3647,34 @@ export function createStorageCore(
|
|
|
3486
3647
|
|
|
3487
3648
|
const tx: TransactionContext = {
|
|
3488
3649
|
scope,
|
|
3489
|
-
getRaw: (key) =>
|
|
3650
|
+
getRaw: (key) => {
|
|
3651
|
+
assertTransactionOpen();
|
|
3652
|
+
return getRawValue(key, scope);
|
|
3653
|
+
},
|
|
3490
3654
|
setRaw: (key, value) => {
|
|
3655
|
+
assertTransactionOpen();
|
|
3491
3656
|
rememberRollback(key);
|
|
3492
3657
|
setRawValue(key, value, scope);
|
|
3493
3658
|
},
|
|
3494
3659
|
removeRaw: (key) => {
|
|
3660
|
+
assertTransactionOpen();
|
|
3495
3661
|
rememberRollback(key, undefined, scope === StorageScope.Secure);
|
|
3496
3662
|
removeRawValue(key, scope);
|
|
3497
3663
|
},
|
|
3498
3664
|
getItem: (item) => {
|
|
3665
|
+
assertTransactionOpen();
|
|
3499
3666
|
assertBatchScope([item], scope);
|
|
3500
3667
|
rememberItemRollback(item);
|
|
3501
3668
|
return item.get();
|
|
3502
3669
|
},
|
|
3503
3670
|
setItem: (item, value) => {
|
|
3671
|
+
assertTransactionOpen();
|
|
3504
3672
|
assertBatchScope([item], scope);
|
|
3505
3673
|
rememberItemRollback(item);
|
|
3506
3674
|
item.set(value);
|
|
3507
3675
|
},
|
|
3508
3676
|
removeItem: (item) => {
|
|
3677
|
+
assertTransactionOpen();
|
|
3509
3678
|
assertBatchScope([item], scope);
|
|
3510
3679
|
rememberItemRollback(item);
|
|
3511
3680
|
item.delete();
|
|
@@ -3513,8 +3682,12 @@ export function createStorageCore(
|
|
|
3513
3682
|
};
|
|
3514
3683
|
|
|
3515
3684
|
try {
|
|
3516
|
-
|
|
3685
|
+
const result = transaction(tx);
|
|
3686
|
+
assertSynchronousCallbackResult(result, "Transaction");
|
|
3687
|
+
transactionOpen = false;
|
|
3688
|
+
return result;
|
|
3517
3689
|
} catch (error) {
|
|
3690
|
+
transactionOpen = false;
|
|
3518
3691
|
itemRenameStates.forEach((migrated, item) => {
|
|
3519
3692
|
item._setRenameMigrationState(migrated);
|
|
3520
3693
|
});
|
|
@@ -3897,6 +4070,7 @@ export function createStorageCore(
|
|
|
3897
4070
|
removeBatch,
|
|
3898
4071
|
registerMigration,
|
|
3899
4072
|
migrateToLatest,
|
|
4073
|
+
migrateSecureAccessControl,
|
|
3900
4074
|
runTransaction,
|
|
3901
4075
|
createSecureAuthStorage,
|
|
3902
4076
|
internals,
|
package/src/storage-platform.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { AccessControl, StorageScope } from "./Storage.types";
|
|
|
7
7
|
|
|
8
8
|
export type PlatformStorage = StorageCore["storage"] & {
|
|
9
9
|
setAccessControl(level: AccessControl): void;
|
|
10
|
+
isProtectedDataAvailable(): boolean;
|
|
11
|
+
onProtectedDataAvailable(listener: () => void): () => void;
|
|
10
12
|
setSecureWritesAsync(enabled: boolean): void;
|
|
11
13
|
setKeychainAccessGroup(group: string): void;
|
|
12
14
|
getCapabilities(): StorageCapabilities;
|