@kokuin/expo 0.2.1 → 0.3.1

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 CHANGED
@@ -1,9 +1,13 @@
1
- import type { KeyEntry } from '@kokuin/token';
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 declare class ExpoKeyEntry implements KeyEntry<Uint8Array> {
4
+ export type ExpoKeyEntryParams = {
5
+ keyID: string;
6
+ options?: StoreEntryOptions;
7
+ };
8
+ export declare class ExpoKeyEntry implements MutableKeyEntry<Uint8Array> {
5
9
  #private;
6
- constructor(keyID: string, options?: StoreEntryOptions);
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
- constructor(keyID, options){
8
- this.#keyID = keyID;
9
- this.#options = options;
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
- async provideAsync() {
38
- const existing = await this.getAsync();
39
- if (existing != null) {
40
- return existing;
41
- }
42
- const privateKey = await randomPrivateKeyAsync();
43
- await this.setAsync(privateKey);
44
- return privateKey;
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 { KeyStore } from '@kokuin/token';
2
- import { ExpoKeyEntry } from './entry.js';
3
- export declare const ExpoKeyStore: KeyStore<Uint8Array, ExpoKeyEntry>;
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
- export const ExpoKeyStore = {
3
- entry: (keyID, options)=>{
4
- return new ExpoKeyEntry(keyID, options);
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,7 +1,6 @@
1
1
  {
2
2
  "name": "@kokuin/expo",
3
- "version": "0.2.1",
4
- "license": "MIT",
3
+ "version": "0.3.1",
5
4
  "description": "Key store for React Native",
6
5
  "keywords": [
7
6
  "cryptography",
@@ -15,32 +14,39 @@
15
14
  "url": "https://github.com/TairuFramework/kokuin",
16
15
  "directory": "packages/expo"
17
16
  },
17
+ "license": "MIT",
18
+ "sideEffects": false,
18
19
  "type": "module",
19
- "main": "lib/index.js",
20
- "types": "lib/index.d.ts",
21
20
  "exports": {
22
21
  ".": "./lib/index.js"
23
22
  },
23
+ "main": "lib/index.js",
24
+ "types": "lib/index.d.ts",
24
25
  "files": [
25
26
  "lib/*"
26
27
  ],
27
- "sideEffects": false,
28
28
  "dependencies": {
29
- "@sozai/codec": "^0.2.0",
30
- "@sozai/log": "^0.1.0",
31
- "@sozai/otel": "^0.2.0",
32
- "expo-crypto": "^57.0.0",
33
- "expo-secure-store": "^57.0.0",
34
- "@kokuin/otel": "^0.1.0",
35
- "@kokuin/token": "^0.2.0"
29
+ "@sozai/codec": "^0.4.0",
30
+ "@sozai/log": "^0.3.1",
31
+ "@sozai/otel": "^0.3.2",
32
+ "expo-crypto": "^57.0.1",
33
+ "expo-secure-store": "^57.0.1",
34
+ "@kokuin/otel": "^0.3.0",
35
+ "@kokuin/token": "^0.4.0"
36
+ },
37
+ "devDependencies": {
38
+ "@kokuin/keystore-conformance": "^0.0.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
36
42
  },
37
43
  "scripts": {
44
+ "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
38
45
  "build:clean": "del lib",
39
46
  "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
40
47
  "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
41
- "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
48
+ "test": "pnpm run test:types && pnpm run test:unit",
42
49
  "test:types": "tsc --noEmit --skipLibCheck -p tsconfig.test.json",
43
- "test:unit": "vitest run",
44
- "test": "pnpm run test:types && pnpm run test:unit"
50
+ "test:unit": "vitest run"
45
51
  }
46
52
  }
package/lib/identity.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import { type FullIdentity } from '@kokuin/token';
2
- export declare function provideFullIdentity(keyID: string): FullIdentity;
3
- export declare function provideFullIdentityAsync(keyID: string): Promise<FullIdentity>;
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
- }