@kokuin/expo 0.2.0 → 0.3.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/lib/entry.d.ts +12 -3
- package/lib/entry.js +34 -11
- package/lib/index.d.ts +1 -2
- package/lib/index.js +0 -1
- package/lib/store.d.ts +19 -3
- package/lib/store.js +73 -4
- package/package.json +11 -8
- package/lib/identity.d.ts +0 -3
- package/lib/identity.js +0 -58
package/lib/entry.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MutableKeyEntry } from '@kokuin/token';
|
|
2
2
|
import * as SecureStore from 'expo-secure-store';
|
|
3
3
|
export type StoreEntryOptions = SecureStore.SecureStoreOptions;
|
|
4
|
-
export
|
|
4
|
+
export type ExpoKeyEntryParams = {
|
|
5
|
+
keyID: string;
|
|
6
|
+
options?: StoreEntryOptions;
|
|
7
|
+
};
|
|
8
|
+
export declare class ExpoKeyEntry implements MutableKeyEntry<Uint8Array> {
|
|
5
9
|
#private;
|
|
6
|
-
constructor(
|
|
10
|
+
constructor(params: ExpoKeyEntryParams);
|
|
7
11
|
get keyID(): string;
|
|
8
12
|
get(): Uint8Array | null;
|
|
9
13
|
getAsync(): Promise<Uint8Array | null>;
|
|
@@ -11,5 +15,10 @@ export declare class ExpoKeyEntry implements KeyEntry<Uint8Array> {
|
|
|
11
15
|
setAsync(privateKey: Uint8Array): Promise<void>;
|
|
12
16
|
provide(): Uint8Array;
|
|
13
17
|
provideAsync(): Promise<Uint8Array>;
|
|
18
|
+
/**
|
|
19
|
+
* Delete the key. `expo-secure-store` has no synchronous delete, so this starts the deletion
|
|
20
|
+
* and returns immediately — use {@link removeAsync} when you need to know it completed.
|
|
21
|
+
*/
|
|
22
|
+
remove(): void;
|
|
14
23
|
removeAsync(): Promise<void>;
|
|
15
24
|
}
|
package/lib/entry.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { fromB64, toB64 } from '@sozai/codec';
|
|
2
|
+
import { getLogger } from '@sozai/log';
|
|
2
3
|
import * as SecureStore from 'expo-secure-store';
|
|
3
4
|
import { randomPrivateKey, randomPrivateKeyAsync } from './utils.js';
|
|
5
|
+
const logger = getLogger([
|
|
6
|
+
'kokuin',
|
|
7
|
+
'expo'
|
|
8
|
+
]);
|
|
4
9
|
export class ExpoKeyEntry {
|
|
5
10
|
#keyID;
|
|
6
11
|
#options;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
// Serializes provideAsync within this process. Expo runs a single app process, so there is
|
|
13
|
+
// no cross-process race to guard — unlike node and electron.
|
|
14
|
+
#provideLock = Promise.resolve();
|
|
15
|
+
constructor(params){
|
|
16
|
+
this.#keyID = params.keyID;
|
|
17
|
+
this.#options = params.options;
|
|
10
18
|
}
|
|
11
19
|
get keyID() {
|
|
12
20
|
return this.#keyID;
|
|
@@ -34,14 +42,29 @@ export class ExpoKeyEntry {
|
|
|
34
42
|
this.set(privateKey);
|
|
35
43
|
return privateKey;
|
|
36
44
|
}
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
provideAsync() {
|
|
46
|
+
const run = this.#provideLock.then(async ()=>{
|
|
47
|
+
const existing = await this.getAsync();
|
|
48
|
+
if (existing != null) {
|
|
49
|
+
return existing;
|
|
50
|
+
}
|
|
51
|
+
const privateKey = await randomPrivateKeyAsync();
|
|
52
|
+
await this.setAsync(privateKey);
|
|
53
|
+
return privateKey;
|
|
54
|
+
});
|
|
55
|
+
this.#provideLock = run.catch(()=>undefined);
|
|
56
|
+
return run;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Delete the key. `expo-secure-store` has no synchronous delete, so this starts the deletion
|
|
60
|
+
* and returns immediately — use {@link removeAsync} when you need to know it completed.
|
|
61
|
+
*/ remove() {
|
|
62
|
+
SecureStore.deleteItemAsync(this.#keyID, this.#options).catch((error)=>{
|
|
63
|
+
logger.warn('Failed to remove key {keyID}: {error}', {
|
|
64
|
+
keyID: this.#keyID,
|
|
65
|
+
error
|
|
66
|
+
});
|
|
67
|
+
});
|
|
45
68
|
}
|
|
46
69
|
async removeAsync() {
|
|
47
70
|
await SecureStore.deleteItemAsync(this.#keyID, this.#options);
|
package/lib/index.d.ts
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module expo-keystore
|
|
11
11
|
*/
|
|
12
|
-
export { ExpoKeyEntry } from './entry.js';
|
|
13
|
-
export { provideFullIdentity, provideFullIdentityAsync } from './identity.js';
|
|
12
|
+
export { ExpoKeyEntry, type ExpoKeyEntryParams, type StoreEntryOptions } from './entry.js';
|
|
14
13
|
export { ExpoKeyStore } from './store.js';
|
|
15
14
|
export { randomPrivateKey, randomPrivateKeyAsync } from './utils.js';
|
package/lib/index.js
CHANGED
|
@@ -9,6 +9,5 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module expo-keystore
|
|
11
11
|
*/ export { ExpoKeyEntry } from './entry.js';
|
|
12
|
-
export { provideFullIdentity, provideFullIdentityAsync } from './identity.js';
|
|
13
12
|
export { ExpoKeyStore } from './store.js';
|
|
14
13
|
export { randomPrivateKey, randomPrivateKeyAsync } from './utils.js';
|
package/lib/store.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { ExpoKeyEntry } from './entry.js';
|
|
3
|
-
export declare
|
|
1
|
+
import { type FullIdentity, type IdentityProvider, type KeyStore } from '@kokuin/token';
|
|
2
|
+
import { ExpoKeyEntry, type StoreEntryOptions } from './entry.js';
|
|
3
|
+
export declare class ExpoKeyStore implements KeyStore<Uint8Array, ExpoKeyEntry>, IdentityProvider<FullIdentity> {
|
|
4
|
+
#private;
|
|
5
|
+
/** The process-wide store. `options` apply to every entry it creates. */
|
|
6
|
+
static open(options?: StoreEntryOptions): ExpoKeyStore;
|
|
7
|
+
constructor(options?: StoreEntryOptions);
|
|
8
|
+
/**
|
|
9
|
+
* The entry for `keyID`, cached — `entry(x) === entry(x)`.
|
|
10
|
+
*
|
|
11
|
+
* The old object-literal store created a fresh entry per call, which silently defeated the
|
|
12
|
+
* per-entry `provideAsync` lock, and took an undeclared second `options` argument that the
|
|
13
|
+
* `KeyStore` type does not have. Options now belong to the store.
|
|
14
|
+
*/
|
|
15
|
+
entry(keyID: string): ExpoKeyEntry;
|
|
16
|
+
provideIdentity(keyID: string): Promise<FullIdentity>;
|
|
17
|
+
/** {@link provideIdentity}, synchronously. Beyond the `IdentityProvider` contract. */
|
|
18
|
+
provideIdentitySync(keyID: string): FullIdentity;
|
|
19
|
+
}
|
package/lib/store.js
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
|
+
import { createTracer, KokuinAttributeKeys, KokuinSpanNames } from '@kokuin/otel';
|
|
2
|
+
import { createFullIdentity } from '@kokuin/token';
|
|
3
|
+
import { getLogger } from '@sozai/log';
|
|
4
|
+
import { withSpan, withSyncSpan } from '@sozai/otel';
|
|
1
5
|
import { ExpoKeyEntry } from './entry.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
const tracer = createTracer('keystore.expo');
|
|
7
|
+
const logger = getLogger([
|
|
8
|
+
'kokuin',
|
|
9
|
+
'expo'
|
|
10
|
+
]);
|
|
11
|
+
export class ExpoKeyStore {
|
|
12
|
+
static #default;
|
|
13
|
+
/** The process-wide store. `options` apply to every entry it creates. */ static open(options) {
|
|
14
|
+
ExpoKeyStore.#default ??= new ExpoKeyStore(options);
|
|
15
|
+
return ExpoKeyStore.#default;
|
|
5
16
|
}
|
|
6
|
-
|
|
17
|
+
#entries = Object.create(null);
|
|
18
|
+
#options;
|
|
19
|
+
constructor(options){
|
|
20
|
+
this.#options = options;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The entry for `keyID`, cached — `entry(x) === entry(x)`.
|
|
24
|
+
*
|
|
25
|
+
* The old object-literal store created a fresh entry per call, which silently defeated the
|
|
26
|
+
* per-entry `provideAsync` lock, and took an undeclared second `options` argument that the
|
|
27
|
+
* `KeyStore` type does not have. Options now belong to the store.
|
|
28
|
+
*/ entry(keyID) {
|
|
29
|
+
this.#entries[keyID] ??= new ExpoKeyEntry({
|
|
30
|
+
keyID,
|
|
31
|
+
options: this.#options
|
|
32
|
+
});
|
|
33
|
+
return this.#entries[keyID];
|
|
34
|
+
}
|
|
35
|
+
async provideIdentity(keyID) {
|
|
36
|
+
return withSpan(tracer, KokuinSpanNames.KEYSTORE_GET_OR_CREATE, {
|
|
37
|
+
attributes: {
|
|
38
|
+
[KokuinAttributeKeys.KEYSTORE_STORE_TYPE]: 'expo'
|
|
39
|
+
}
|
|
40
|
+
}, async (span)=>{
|
|
41
|
+
const entry = this.entry(keyID);
|
|
42
|
+
const existing = await entry.getAsync();
|
|
43
|
+
const privateKey = existing ?? await entry.provideAsync();
|
|
44
|
+
const identity = createFullIdentity(privateKey);
|
|
45
|
+
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
46
|
+
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, existing == null);
|
|
47
|
+
if (existing == null) {
|
|
48
|
+
logger.info('New identity generated: {did}', {
|
|
49
|
+
did: identity.id
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return identity;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** {@link provideIdentity}, synchronously. Beyond the `IdentityProvider` contract. */ provideIdentitySync(keyID) {
|
|
56
|
+
return withSyncSpan(tracer, KokuinSpanNames.KEYSTORE_GET_OR_CREATE, {
|
|
57
|
+
attributes: {
|
|
58
|
+
[KokuinAttributeKeys.KEYSTORE_STORE_TYPE]: 'expo'
|
|
59
|
+
}
|
|
60
|
+
}, (span)=>{
|
|
61
|
+
const entry = this.entry(keyID);
|
|
62
|
+
const existing = entry.get();
|
|
63
|
+
const privateKey = existing ?? entry.provide();
|
|
64
|
+
const identity = createFullIdentity(privateKey);
|
|
65
|
+
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
66
|
+
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, existing == null);
|
|
67
|
+
if (existing == null) {
|
|
68
|
+
logger.info('New identity generated: {did}', {
|
|
69
|
+
did: identity.id
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return identity;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kokuin/expo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Key store for React Native",
|
|
6
6
|
"keywords": [
|
|
@@ -26,13 +26,16 @@
|
|
|
26
26
|
],
|
|
27
27
|
"sideEffects": false,
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@sozai/codec": "^0.
|
|
30
|
-
"@sozai/log": "^0.
|
|
31
|
-
"@sozai/otel": "^0.
|
|
32
|
-
"expo-crypto": "^57.0.
|
|
33
|
-
"expo-secure-store": "^57.0.
|
|
34
|
-
"@kokuin/otel": "^0.
|
|
35
|
-
"@kokuin/token": "^0.
|
|
29
|
+
"@sozai/codec": "^0.2.0",
|
|
30
|
+
"@sozai/log": "^0.2.0",
|
|
31
|
+
"@sozai/otel": "^0.3.0",
|
|
32
|
+
"expo-crypto": "^57.0.1",
|
|
33
|
+
"expo-secure-store": "^57.0.1",
|
|
34
|
+
"@kokuin/otel": "^0.3.0",
|
|
35
|
+
"@kokuin/token": "^0.3.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@kokuin/keystore-conformance": "^0.0.0"
|
|
36
39
|
},
|
|
37
40
|
"scripts": {
|
|
38
41
|
"build:clean": "del lib",
|
package/lib/identity.d.ts
DELETED
package/lib/identity.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { createTracer, KokuinAttributeKeys, KokuinSpanNames } from '@kokuin/otel';
|
|
2
|
-
import { createFullIdentity } from '@kokuin/token';
|
|
3
|
-
import { getLogger } from '@sozai/log';
|
|
4
|
-
import { withSpan, withSyncSpan } from '@sozai/otel';
|
|
5
|
-
import { ExpoKeyStore } from './store.js';
|
|
6
|
-
const tracer = createTracer('keystore.expo');
|
|
7
|
-
const logger = getLogger([
|
|
8
|
-
'kokuin',
|
|
9
|
-
'expo'
|
|
10
|
-
]);
|
|
11
|
-
export function provideFullIdentity(keyID) {
|
|
12
|
-
return withSyncSpan(tracer, KokuinSpanNames.KEYSTORE_GET_OR_CREATE, {
|
|
13
|
-
attributes: {
|
|
14
|
-
[KokuinAttributeKeys.KEYSTORE_STORE_TYPE]: 'expo'
|
|
15
|
-
}
|
|
16
|
-
}, (span)=>{
|
|
17
|
-
const entry = ExpoKeyStore.entry(keyID);
|
|
18
|
-
const existing = entry.get();
|
|
19
|
-
if (existing != null) {
|
|
20
|
-
const identity = createFullIdentity(existing);
|
|
21
|
-
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
22
|
-
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, false);
|
|
23
|
-
return identity;
|
|
24
|
-
}
|
|
25
|
-
const key = entry.provide();
|
|
26
|
-
const identity = createFullIdentity(key);
|
|
27
|
-
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
28
|
-
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, true);
|
|
29
|
-
logger.info('New identity generated: {did}', {
|
|
30
|
-
did: identity.id
|
|
31
|
-
});
|
|
32
|
-
return identity;
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
export async function provideFullIdentityAsync(keyID) {
|
|
36
|
-
return withSpan(tracer, KokuinSpanNames.KEYSTORE_GET_OR_CREATE, {
|
|
37
|
-
attributes: {
|
|
38
|
-
[KokuinAttributeKeys.KEYSTORE_STORE_TYPE]: 'expo'
|
|
39
|
-
}
|
|
40
|
-
}, async (span)=>{
|
|
41
|
-
const entry = ExpoKeyStore.entry(keyID);
|
|
42
|
-
const existing = await entry.getAsync();
|
|
43
|
-
if (existing != null) {
|
|
44
|
-
const identity = createFullIdentity(existing);
|
|
45
|
-
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
46
|
-
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, false);
|
|
47
|
-
return identity;
|
|
48
|
-
}
|
|
49
|
-
const key = await entry.provideAsync();
|
|
50
|
-
const identity = createFullIdentity(key);
|
|
51
|
-
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
52
|
-
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, true);
|
|
53
|
-
logger.info('New identity generated: {did}', {
|
|
54
|
-
did: identity.id
|
|
55
|
-
});
|
|
56
|
-
return identity;
|
|
57
|
-
});
|
|
58
|
-
}
|