feat(mobile): add U2F support for Android devices (#2311)
* feat(mobile): add U2F support for Android devices * chore: fix specs
This commit is contained in:
BIN
.yarn/cache/@standardnotes-domain-core-npm-1.12.0-c4ef3cb073-852f15e481.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-domain-core-npm-1.12.0-c4ef3cb073-852f15e481.zip
vendored
Normal file
Binary file not shown.
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standardnotes/common": "^1.46.6",
|
"@standardnotes/common": "^1.46.6",
|
||||||
"@standardnotes/domain-core": "^1.11.3",
|
"@standardnotes/domain-core": "^1.12.0",
|
||||||
"@standardnotes/encryption": "workspace:*",
|
"@standardnotes/encryption": "workspace:*",
|
||||||
"@standardnotes/models": "workspace:*",
|
"@standardnotes/models": "workspace:*",
|
||||||
"@standardnotes/responses": "workspace:*",
|
"@standardnotes/responses": "workspace:*",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standardnotes/common": "^1.46.6",
|
"@standardnotes/common": "^1.46.6",
|
||||||
"@standardnotes/domain-core": "^1.11.3",
|
"@standardnotes/domain-core": "^1.12.0",
|
||||||
"@standardnotes/security": "^1.7.6",
|
"@standardnotes/security": "^1.7.6",
|
||||||
"reflect-metadata": "^0.1.13"
|
"reflect-metadata": "^0.1.13"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ dependencies {
|
|||||||
implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
|
implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
|
||||||
implementation 'de.undercouch:gradle-download-task:5.0.2'
|
implementation 'de.undercouch:gradle-download-task:5.0.2'
|
||||||
|
implementation 'com.google.android.gms:play-services-fido:20.0.1'
|
||||||
|
|
||||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package com.standardnotes;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentSender;
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.util.Log;
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.NativeModule;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ActivityEventListener;
|
||||||
|
import com.facebook.react.bridge.BaseActivityEventListener;
|
||||||
|
import com.facebook.react.bridge.ReactContext;
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
import com.facebook.react.bridge.Promise;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import com.google.android.gms.fido.fido2.Fido2ApiClient;
|
||||||
|
import com.google.android.gms.fido.fido2.api.common.AuthenticatorErrorResponse;
|
||||||
|
import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse;
|
||||||
|
import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensionsClientOutputs;
|
||||||
|
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential;
|
||||||
|
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialType;
|
||||||
|
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor;
|
||||||
|
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions;
|
||||||
|
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions;
|
||||||
|
import com.google.android.gms.fido.Fido;
|
||||||
|
import com.google.android.gms.tasks.OnFailureListener;
|
||||||
|
import com.google.android.gms.tasks.OnSuccessListener;
|
||||||
|
import com.google.android.gms.tasks.Task;
|
||||||
|
|
||||||
|
public class Fido2ApiModule extends ReactContextBaseJavaModule {
|
||||||
|
private final Fido2ApiClient fido2ApiClient;
|
||||||
|
private final ReactApplicationContext reactContext;
|
||||||
|
private static final int SIGN_REQUEST_CODE = 111;
|
||||||
|
|
||||||
|
private static final String LOGS_TAG = "Fido2ApiModule";
|
||||||
|
private static final String RP_ID = "app.standardnotes.com";
|
||||||
|
|
||||||
|
private Promise signInPromise;
|
||||||
|
|
||||||
|
private final ActivityEventListener activityEventListener = new BaseActivityEventListener() {
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
|
||||||
|
super.onActivityResult(activity, requestCode, resultCode, intent);
|
||||||
|
|
||||||
|
if (requestCode == SIGN_REQUEST_CODE) {
|
||||||
|
if (signInPromise != null) {
|
||||||
|
if (resultCode == Activity.RESULT_CANCELED) {
|
||||||
|
Log.e(LOGS_TAG, "FIDO sign in cancelled");
|
||||||
|
|
||||||
|
signInPromise.reject("FIDO sign in cancelled");
|
||||||
|
} else if (resultCode == Activity.RESULT_OK) {
|
||||||
|
if (intent.hasExtra(Fido.FIDO2_KEY_ERROR_EXTRA)) {
|
||||||
|
AuthenticatorErrorResponse authenticatorErrorResponse =
|
||||||
|
AuthenticatorErrorResponse.deserializeFromBytes(intent.getByteArrayExtra(Fido.FIDO2_KEY_ERROR_EXTRA));
|
||||||
|
Log.e(LOGS_TAG, "Fido Error: " + authenticatorErrorResponse.getErrorMessage());
|
||||||
|
|
||||||
|
signInPromise.reject(authenticatorErrorResponse.getErrorMessage());
|
||||||
|
} else if (intent.hasExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA)) {
|
||||||
|
PublicKeyCredential publicKeyCredential =
|
||||||
|
PublicKeyCredential.deserializeFromBytes(
|
||||||
|
intent.getByteArrayExtra(Fido.FIDO2_KEY_CREDENTIAL_EXTRA));
|
||||||
|
AuthenticatorAssertionResponse signedData =
|
||||||
|
(AuthenticatorAssertionResponse) publicKeyCredential.getResponse();
|
||||||
|
|
||||||
|
WritableMap signInResult = Arguments.createMap();
|
||||||
|
signInResult.putString("id", Base64.encodeToString(signedData.getKeyHandle(), Base64.URL_SAFE));
|
||||||
|
signInResult.putString("rawId", Base64.encodeToString(signedData.getKeyHandle(), Base64.URL_SAFE));
|
||||||
|
|
||||||
|
byte[] extensionOutputsBytes = null;
|
||||||
|
AuthenticationExtensionsClientOutputs extensionOutputs = publicKeyCredential.getClientExtensionResults();
|
||||||
|
if (extensionOutputs != null) {
|
||||||
|
extensionOutputsBytes = extensionOutputs.serializeToBytes();
|
||||||
|
if (extensionOutputsBytes != null) {
|
||||||
|
signInResult.putString("clientExtensionResults", Base64.encodeToString(extensionOutputsBytes, Base64.URL_SAFE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WritableMap response = Arguments.createMap();
|
||||||
|
response.putString("clientDataJSON", Base64.encodeToString(signedData.getClientDataJSON(), Base64.URL_SAFE));
|
||||||
|
response.putString("authenticatorData", Base64.encodeToString(signedData.getAuthenticatorData(), Base64.URL_SAFE));
|
||||||
|
response.putString("signature", Base64.encodeToString(signedData.getSignature(), Base64.URL_SAFE));
|
||||||
|
byte[] userHandle = signedData.getUserHandle();
|
||||||
|
if (userHandle != null) {
|
||||||
|
response.putString("userHandle", Base64.encodeToString(userHandle, Base64.URL_SAFE));
|
||||||
|
}
|
||||||
|
signInResult.putMap("response", response);
|
||||||
|
|
||||||
|
signInPromise.resolve(signInResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signInPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Fido2ApiModule(ReactApplicationContext context) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
fido2ApiClient = Fido.getFido2ApiClient(context);
|
||||||
|
context.addActivityEventListener(activityEventListener);
|
||||||
|
|
||||||
|
this.reactContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Fido2ApiModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void promptForU2FAuthentication(String authenticationOptionsJSONString, Promise promise) throws JSONException {
|
||||||
|
signInPromise = promise;
|
||||||
|
|
||||||
|
JSONObject authenticationOptions = new JSONObject(authenticationOptionsJSONString);
|
||||||
|
|
||||||
|
ArrayList<PublicKeyCredentialDescriptor> allowedKeys = new ArrayList<PublicKeyCredentialDescriptor>();
|
||||||
|
|
||||||
|
JSONArray allowedCredentials = authenticationOptions.getJSONArray("allowCredentials");
|
||||||
|
for (int i = 0, size = allowedCredentials.length(); i < size; i++) {
|
||||||
|
JSONObject allowedCredential = allowedCredentials.getJSONObject(i);
|
||||||
|
allowedKeys.add(
|
||||||
|
new PublicKeyCredentialDescriptor(
|
||||||
|
PublicKeyCredentialType.PUBLIC_KEY.toString(),
|
||||||
|
this.convertBase64URLStringToBytes(allowedCredential.getString("id")),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String challenge = authenticationOptions.getString("challenge");
|
||||||
|
Double timeout = authenticationOptions.getDouble("timeout") / 1000d;
|
||||||
|
|
||||||
|
PublicKeyCredentialRequestOptions.Builder optionsBuilder = new PublicKeyCredentialRequestOptions
|
||||||
|
.Builder()
|
||||||
|
.setRpId(RP_ID)
|
||||||
|
.setAllowList(allowedKeys)
|
||||||
|
.setChallenge(this.convertBase64URLStringToBytes(challenge))
|
||||||
|
.setTimeoutSeconds(timeout);
|
||||||
|
|
||||||
|
PublicKeyCredentialRequestOptions options = optionsBuilder.build();
|
||||||
|
|
||||||
|
Task result = this.fido2ApiClient.getSignPendingIntent(options);
|
||||||
|
|
||||||
|
final Activity activity = this.reactContext.getCurrentActivity();
|
||||||
|
|
||||||
|
result.addOnSuccessListener(
|
||||||
|
new OnSuccessListener<PendingIntent>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(PendingIntent fido2PendingIntent) {
|
||||||
|
if (fido2PendingIntent == null) {
|
||||||
|
Log.e(LOGS_TAG, "No pending FIDO intent returned");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
activity.startIntentSenderForResult(
|
||||||
|
fido2PendingIntent.getIntentSender(),
|
||||||
|
SIGN_REQUEST_CODE,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
} catch (IntentSender.SendIntentException exception) {
|
||||||
|
Log.e(LOGS_TAG, "Error starting FIDO intent: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
result.addOnFailureListener(
|
||||||
|
new OnFailureListener() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(Exception e) {
|
||||||
|
Log.e(LOGS_TAG, "Error getting FIDO intent: " + e.getMessage());
|
||||||
|
signInPromise.reject(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] convertBase64URLStringToBytes(String base64URLString) {
|
||||||
|
String base64String = base64URLString.replace('-', '+').replace('_', '/');
|
||||||
|
int padding = (4 - (base64String.length() % 4)) % 4;
|
||||||
|
for (int i = 0; i < padding; i++) {
|
||||||
|
base64String += '=';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base64.decode(base64String, Base64.DEFAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.standardnotes;
|
||||||
|
|
||||||
|
import com.facebook.react.ReactPackage;
|
||||||
|
import com.facebook.react.bridge.NativeModule;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.uimanager.ViewManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Fido2ApiPackage implements ReactPackage {
|
||||||
|
@Override
|
||||||
|
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||||
|
List<NativeModule> modules = new ArrayList<>();
|
||||||
|
|
||||||
|
modules.add(new Fido2ApiModule(reactContext));
|
||||||
|
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,8 @@ public class MainApplication extends Application implements ReactApplication {
|
|||||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||||
|
|
||||||
|
packages.add(new Fido2ApiPackage());
|
||||||
|
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,4 +43,7 @@ newArchEnabled=false
|
|||||||
hermesEnabled=true
|
hermesEnabled=true
|
||||||
|
|
||||||
# Set AsyncStorage limit
|
# Set AsyncStorage limit
|
||||||
AsyncStorage_db_size_in_MB=50
|
AsyncStorage_db_size_in_MB=50
|
||||||
|
|
||||||
|
# The URL of the server
|
||||||
|
host=https://app.standardnotes.com
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
AppStateStatus,
|
AppStateStatus,
|
||||||
ColorSchemeName,
|
ColorSchemeName,
|
||||||
Linking,
|
Linking,
|
||||||
|
NativeModules,
|
||||||
PermissionsAndroid,
|
PermissionsAndroid,
|
||||||
Platform,
|
Platform,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
@@ -71,6 +72,26 @@ export class MobileDevice implements MobileDeviceInterface {
|
|||||||
private colorSchemeService?: ColorSchemeObserverService,
|
private colorSchemeService?: ColorSchemeObserverService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async authenticateWithU2F(authenticationOptionsJSONString: string): Promise<Record<string, unknown> | null> {
|
||||||
|
const { Fido2ApiModule } = NativeModules
|
||||||
|
|
||||||
|
if (!Fido2ApiModule) {
|
||||||
|
this.consoleLog('Fido2ApiModule is not available')
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await Fido2ApiModule.promptForU2FAuthentication(authenticationOptionsJSONString)
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
this.consoleLog(`Fido2ApiModule.authenticateWithU2F error: ${(error as Error).message}`)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
purchaseSubscriptionIAP(plan: AppleIAPProductId): Promise<AppleIAPReceipt | undefined> {
|
purchaseSubscriptionIAP(plan: AppleIAPProductId): Promise<AppleIAPReceipt | undefined> {
|
||||||
return PurchaseManager.getInstance().purchase(plan)
|
return PurchaseManager.getInstance().purchase(plan)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standardnotes/api": "workspace:^",
|
"@standardnotes/api": "workspace:^",
|
||||||
"@standardnotes/common": "^1.46.4",
|
"@standardnotes/common": "^1.46.4",
|
||||||
"@standardnotes/domain-core": "^1.11.3",
|
"@standardnotes/domain-core": "^1.12.0",
|
||||||
"@standardnotes/encryption": "workspace:^",
|
"@standardnotes/encryption": "workspace:^",
|
||||||
"@standardnotes/files": "workspace:^",
|
"@standardnotes/files": "workspace:^",
|
||||||
"@standardnotes/models": "workspace:^",
|
"@standardnotes/models": "workspace:^",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { Environment, Platform, RawKeychainValue } from '@standardnotes/models'
|
||||||
|
|
||||||
import { AppleIAPProductId } from './../Subscription/AppleIAPProductId'
|
import { AppleIAPProductId } from './../Subscription/AppleIAPProductId'
|
||||||
import { DeviceInterface } from './DeviceInterface'
|
import { DeviceInterface } from './DeviceInterface'
|
||||||
import { Environment, Platform, RawKeychainValue } from '@standardnotes/models'
|
|
||||||
import { AppleIAPReceipt } from '../Subscription/AppleIAPReceipt'
|
import { AppleIAPReceipt } from '../Subscription/AppleIAPReceipt'
|
||||||
|
|
||||||
export interface MobileDeviceInterface extends DeviceInterface {
|
export interface MobileDeviceInterface extends DeviceInterface {
|
||||||
@@ -25,4 +26,5 @@ export interface MobileDeviceInterface extends DeviceInterface {
|
|||||||
getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'>
|
getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'>
|
||||||
getColorScheme(): Promise<'light' | 'dark' | null | undefined>
|
getColorScheme(): Promise<'light' | 'dark' | null | undefined>
|
||||||
purchaseSubscriptionIAP(plan: AppleIAPProductId): Promise<AppleIAPReceipt | undefined>
|
purchaseSubscriptionIAP(plan: AppleIAPProductId): Promise<AppleIAPReceipt | undefined>
|
||||||
|
authenticateWithU2F(authenticationOptionsJSONString: string): Promise<Record<string, unknown> | null>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ import { ListRevisions } from '@Lib/Domain/UseCase/ListRevisions/ListRevisions'
|
|||||||
import { GetRevision } from '@Lib/Domain/UseCase/GetRevision/GetRevision'
|
import { GetRevision } from '@Lib/Domain/UseCase/GetRevision/GetRevision'
|
||||||
import { DeleteRevision } from '@Lib/Domain/UseCase/DeleteRevision/DeleteRevision'
|
import { DeleteRevision } from '@Lib/Domain/UseCase/DeleteRevision/DeleteRevision'
|
||||||
import { GetAuthenticatorAuthenticationResponse } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse'
|
import { GetAuthenticatorAuthenticationResponse } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse'
|
||||||
|
import { GetAuthenticatorAuthenticationOptions } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions'
|
||||||
|
|
||||||
/** How often to automatically sync, in milliseconds */
|
/** How often to automatically sync, in milliseconds */
|
||||||
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
|
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
|
||||||
@@ -182,6 +183,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
private declare _addAuthenticator: AddAuthenticator
|
private declare _addAuthenticator: AddAuthenticator
|
||||||
private declare _listAuthenticators: ListAuthenticators
|
private declare _listAuthenticators: ListAuthenticators
|
||||||
private declare _deleteAuthenticator: DeleteAuthenticator
|
private declare _deleteAuthenticator: DeleteAuthenticator
|
||||||
|
private declare _getAuthenticatorAuthenticationOptions: GetAuthenticatorAuthenticationOptions
|
||||||
private declare _getAuthenticatorAuthenticationResponse: GetAuthenticatorAuthenticationResponse
|
private declare _getAuthenticatorAuthenticationResponse: GetAuthenticatorAuthenticationResponse
|
||||||
private declare _listRevisions: ListRevisions
|
private declare _listRevisions: ListRevisions
|
||||||
private declare _getRevision: GetRevision
|
private declare _getRevision: GetRevision
|
||||||
@@ -284,6 +286,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
return this._deleteAuthenticator
|
return this._deleteAuthenticator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get getAuthenticatorAuthenticationOptions(): GetAuthenticatorAuthenticationOptions {
|
||||||
|
return this._getAuthenticatorAuthenticationOptions
|
||||||
|
}
|
||||||
|
|
||||||
get getAuthenticatorAuthenticationResponse(): GetAuthenticatorAuthenticationResponse {
|
get getAuthenticatorAuthenticationResponse(): GetAuthenticatorAuthenticationResponse {
|
||||||
return this._getAuthenticatorAuthenticationResponse
|
return this._getAuthenticatorAuthenticationResponse
|
||||||
}
|
}
|
||||||
@@ -1819,8 +1825,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
|
|
||||||
this._deleteAuthenticator = new DeleteAuthenticator(this.authenticatorManager)
|
this._deleteAuthenticator = new DeleteAuthenticator(this.authenticatorManager)
|
||||||
|
|
||||||
|
this._getAuthenticatorAuthenticationOptions = new GetAuthenticatorAuthenticationOptions(this.authenticatorManager)
|
||||||
|
|
||||||
this._getAuthenticatorAuthenticationResponse = new GetAuthenticatorAuthenticationResponse(
|
this._getAuthenticatorAuthenticationResponse = new GetAuthenticatorAuthenticationResponse(
|
||||||
this.authenticatorManager,
|
this._getAuthenticatorAuthenticationOptions,
|
||||||
this.options.u2fAuthenticatorVerificationPromptFunction,
|
this.options.u2fAuthenticatorVerificationPromptFunction,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { AuthenticatorClientInterface } from '@standardnotes/services'
|
||||||
|
|
||||||
|
import { GetAuthenticatorAuthenticationOptions } from './GetAuthenticatorAuthenticationOptions'
|
||||||
|
|
||||||
|
describe('GetAuthenticatorAuthenticationOptions', () => {
|
||||||
|
let authenticatorClient: AuthenticatorClientInterface
|
||||||
|
|
||||||
|
const createUseCase = () => new GetAuthenticatorAuthenticationOptions(authenticatorClient)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
authenticatorClient = {} as jest.Mocked<AuthenticatorClientInterface>
|
||||||
|
authenticatorClient.generateAuthenticationOptions = jest.fn().mockResolvedValue({ foo: 'bar' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error if username is not provided', async () => {
|
||||||
|
const result = await createUseCase().execute({
|
||||||
|
username: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(true)
|
||||||
|
expect(result.getError()).toBe('Could not generate authenticator authentication options: Username cannot be empty')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error if authenticator client fails to generate authentication options', async () => {
|
||||||
|
authenticatorClient.generateAuthenticationOptions = jest.fn().mockResolvedValue(null)
|
||||||
|
|
||||||
|
const result = await createUseCase().execute({
|
||||||
|
username: 'test@test.te',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(true)
|
||||||
|
expect(result.getError()).toBe('Could not generate authenticator authentication options')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return ok if authenticator client succeeds to generate authenticator response', async () => {
|
||||||
|
const result = await createUseCase().execute({
|
||||||
|
username: 'test@test.te',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.isFailed()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { AuthenticatorClientInterface } from '@standardnotes/services'
|
||||||
|
import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { GetAuthenticatorAuthenticationOptionsDTO } from './GetAuthenticatorAuthenticationOptionsDTO'
|
||||||
|
|
||||||
|
export class GetAuthenticatorAuthenticationOptions implements UseCaseInterface<Record<string, unknown>> {
|
||||||
|
constructor(private authenticatorClient: AuthenticatorClientInterface) {}
|
||||||
|
|
||||||
|
async execute(dto: GetAuthenticatorAuthenticationOptionsDTO): Promise<Result<Record<string, unknown>>> {
|
||||||
|
const usernameOrError = Username.create(dto.username)
|
||||||
|
if (usernameOrError.isFailed()) {
|
||||||
|
return Result.fail(`Could not generate authenticator authentication options: ${usernameOrError.getError()}`)
|
||||||
|
}
|
||||||
|
const username = usernameOrError.getValue()
|
||||||
|
|
||||||
|
const authenticationOptions = await this.authenticatorClient.generateAuthenticationOptions(username)
|
||||||
|
if (authenticationOptions === null) {
|
||||||
|
return Result.fail('Could not generate authenticator authentication options')
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(authenticationOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface GetAuthenticatorAuthenticationOptionsDTO {
|
||||||
|
username: string
|
||||||
|
}
|
||||||
@@ -1,40 +1,31 @@
|
|||||||
import { AuthenticatorClientInterface } from '@standardnotes/services'
|
|
||||||
|
|
||||||
import { GetAuthenticatorAuthenticationResponse } from './GetAuthenticatorAuthenticationResponse'
|
import { GetAuthenticatorAuthenticationResponse } from './GetAuthenticatorAuthenticationResponse'
|
||||||
|
import { GetAuthenticatorAuthenticationOptions } from '../GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions'
|
||||||
|
import { Result } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
describe('GetAuthenticatorAuthenticationResponse', () => {
|
describe('GetAuthenticatorAuthenticationResponse', () => {
|
||||||
let authenticatorClient: AuthenticatorClientInterface
|
let getAuthenticatorAuthenticationOptions: GetAuthenticatorAuthenticationOptions
|
||||||
let authenticatorVerificationPromptFunction: (
|
let authenticatorVerificationPromptFunction: (
|
||||||
authenticationOptions: Record<string, unknown>,
|
authenticationOptions: Record<string, unknown>,
|
||||||
) => Promise<Record<string, unknown>>
|
) => Promise<Record<string, unknown>>
|
||||||
|
|
||||||
const createUseCase = () => new GetAuthenticatorAuthenticationResponse(authenticatorClient, authenticatorVerificationPromptFunction)
|
const createUseCase = () => new GetAuthenticatorAuthenticationResponse(getAuthenticatorAuthenticationOptions, authenticatorVerificationPromptFunction)
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authenticatorClient = {} as jest.Mocked<AuthenticatorClientInterface>
|
getAuthenticatorAuthenticationOptions = {} as jest.Mocked<GetAuthenticatorAuthenticationOptions>
|
||||||
authenticatorClient.generateAuthenticationOptions = jest.fn().mockResolvedValue({ foo: 'bar' })
|
getAuthenticatorAuthenticationOptions.execute = jest.fn().mockResolvedValue(Result.ok({ foo: 'bar' }))
|
||||||
|
|
||||||
authenticatorVerificationPromptFunction = jest.fn()
|
authenticatorVerificationPromptFunction = jest.fn()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an error if username is not provided', async () => {
|
it('should return an error if it fails to generate authentication options', async () => {
|
||||||
const result = await createUseCase().execute({
|
getAuthenticatorAuthenticationOptions.execute = jest.fn().mockReturnValue(Result.fail('error'))
|
||||||
username: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result.isFailed()).toBe(true)
|
|
||||||
expect(result.getError()).toBe('Could not generate authenticator authentication options: Username cannot be empty')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return an error if authenticator client fails to generate authentication options', async () => {
|
|
||||||
authenticatorClient.generateAuthenticationOptions = jest.fn().mockResolvedValue(null)
|
|
||||||
|
|
||||||
const result = await createUseCase().execute({
|
const result = await createUseCase().execute({
|
||||||
username: 'test@test.te',
|
username: 'test@test.te',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.isFailed()).toBe(true)
|
expect(result.isFailed()).toBe(true)
|
||||||
expect(result.getError()).toBe('Could not generate authenticator authentication options')
|
expect(result.getError()).toBe('error')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return an error if authenticator verification prompt function fails', async () => {
|
it('should return an error if authenticator verification prompt function fails', async () => {
|
||||||
@@ -57,7 +48,7 @@ describe('GetAuthenticatorAuthenticationResponse', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return error if authenticatorVerificationPromptFunction is not provided', async () => {
|
it('should return error if authenticatorVerificationPromptFunction is not provided', async () => {
|
||||||
const result = await new GetAuthenticatorAuthenticationResponse(authenticatorClient).execute({
|
const result = await new GetAuthenticatorAuthenticationResponse(getAuthenticatorAuthenticationOptions).execute({
|
||||||
username: 'test@test.te',
|
username: 'test@test.te',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { AuthenticatorClientInterface } from '@standardnotes/services'
|
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
|
||||||
import { GetAuthenticatorAuthenticationResponseDTO } from './GetAuthenticatorAuthenticationResponseDTO'
|
import { GetAuthenticatorAuthenticationResponseDTO } from './GetAuthenticatorAuthenticationResponseDTO'
|
||||||
|
import { GetAuthenticatorAuthenticationOptions } from '../GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions'
|
||||||
|
|
||||||
export class GetAuthenticatorAuthenticationResponse implements UseCaseInterface<Record<string, unknown>> {
|
export class GetAuthenticatorAuthenticationResponse implements UseCaseInterface<Record<string, unknown>> {
|
||||||
constructor(
|
constructor(
|
||||||
private authenticatorClient: AuthenticatorClientInterface,
|
private getAuthenticatorAuthenticationOptions: GetAuthenticatorAuthenticationOptions,
|
||||||
private authenticatorVerificationPromptFunction?: (
|
private authenticatorVerificationPromptFunction?: (
|
||||||
authenticationOptions: Record<string, unknown>,
|
authenticationOptions: Record<string, unknown>,
|
||||||
) => Promise<Record<string, unknown>>,
|
) => Promise<Record<string, unknown>>,
|
||||||
@@ -17,16 +17,13 @@ export class GetAuthenticatorAuthenticationResponse implements UseCaseInterface<
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const usernameOrError = Username.create(dto.username)
|
const authenticationOptionsOrError = await this.getAuthenticatorAuthenticationOptions.execute({
|
||||||
if (usernameOrError.isFailed()) {
|
username: dto.username,
|
||||||
return Result.fail(`Could not generate authenticator authentication options: ${usernameOrError.getError()}`)
|
})
|
||||||
}
|
if (authenticationOptionsOrError.isFailed()) {
|
||||||
const username = usernameOrError.getValue()
|
return Result.fail(authenticationOptionsOrError.getError())
|
||||||
|
|
||||||
const authenticationOptions = await this.authenticatorClient.generateAuthenticationOptions(username)
|
|
||||||
if (authenticationOptions === null) {
|
|
||||||
return Result.fail('Could not generate authenticator authentication options')
|
|
||||||
}
|
}
|
||||||
|
const authenticationOptions = authenticationOptionsOrError.getValue()
|
||||||
|
|
||||||
let authenticatorResponse
|
let authenticatorResponse
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { GetAuthenticatorAuthenticationResponse } from './GetAuthenticatorAuthen
|
|||||||
import { ListRevisions } from './ListRevisions/ListRevisions'
|
import { ListRevisions } from './ListRevisions/ListRevisions'
|
||||||
import { GetRevision } from './GetRevision/GetRevision'
|
import { GetRevision } from './GetRevision/GetRevision'
|
||||||
import { DeleteRevision } from './DeleteRevision/DeleteRevision'
|
import { DeleteRevision } from './DeleteRevision/DeleteRevision'
|
||||||
|
import { GetAuthenticatorAuthenticationOptions } from './GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions'
|
||||||
|
|
||||||
export interface UseCaseContainerInterface {
|
export interface UseCaseContainerInterface {
|
||||||
get signInWithRecoveryCodes(): SignInWithRecoveryCodes
|
get signInWithRecoveryCodes(): SignInWithRecoveryCodes
|
||||||
@@ -15,6 +16,7 @@ export interface UseCaseContainerInterface {
|
|||||||
get listAuthenticators(): ListAuthenticators
|
get listAuthenticators(): ListAuthenticators
|
||||||
get deleteAuthenticator(): DeleteAuthenticator
|
get deleteAuthenticator(): DeleteAuthenticator
|
||||||
get getAuthenticatorAuthenticationResponse(): GetAuthenticatorAuthenticationResponse
|
get getAuthenticatorAuthenticationResponse(): GetAuthenticatorAuthenticationResponse
|
||||||
|
get getAuthenticatorAuthenticationOptions(): GetAuthenticatorAuthenticationOptions
|
||||||
get listRevisions(): ListRevisions
|
get listRevisions(): ListRevisions
|
||||||
get getRevision(): GetRevision
|
get getRevision(): GetRevision
|
||||||
get deleteRevision(): DeleteRevision
|
get deleteRevision(): DeleteRevision
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export * from './UseCase/DeleteAuthenticator/DeleteAuthenticator'
|
|||||||
export * from './UseCase/DeleteAuthenticator/DeleteAuthenticatorDTO'
|
export * from './UseCase/DeleteAuthenticator/DeleteAuthenticatorDTO'
|
||||||
export * from './UseCase/DeleteRevision/DeleteRevision'
|
export * from './UseCase/DeleteRevision/DeleteRevision'
|
||||||
export * from './UseCase/DeleteRevision/DeleteRevisionDTO'
|
export * from './UseCase/DeleteRevision/DeleteRevisionDTO'
|
||||||
|
export * from './UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions'
|
||||||
|
export * from './UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptionsDTO'
|
||||||
export * from './UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse'
|
export * from './UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse'
|
||||||
export * from './UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponseDTO'
|
export * from './UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponseDTO'
|
||||||
export * from './UseCase/GetRecoveryCodes/GetRecoveryCodes'
|
export * from './UseCase/GetRecoveryCodes/GetRecoveryCodes'
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"@babel/preset-env": "*",
|
"@babel/preset-env": "*",
|
||||||
"@standardnotes/api": "workspace:*",
|
"@standardnotes/api": "workspace:*",
|
||||||
"@standardnotes/common": "^1.46.6",
|
"@standardnotes/common": "^1.46.6",
|
||||||
"@standardnotes/domain-core": "^1.11.3",
|
"@standardnotes/domain-core": "^1.12.0",
|
||||||
"@standardnotes/domain-events": "^2.108.1",
|
"@standardnotes/domain-events": "^2.108.1",
|
||||||
"@standardnotes/encryption": "workspace:*",
|
"@standardnotes/encryption": "workspace:*",
|
||||||
"@standardnotes/features": "workspace:*",
|
"@standardnotes/features": "workspace:*",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @jest-environment jsdom
|
* @jest-environment jsdom
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { WebApplicationInterface } from '@standardnotes/snjs/dist/@types'
|
import { WebApplicationInterface } from '@standardnotes/snjs'
|
||||||
import { jsonTestData, htmlTestData } from './testData'
|
import { jsonTestData, htmlTestData } from './testData'
|
||||||
import { GoogleKeepConverter } from './GoogleKeepConverter'
|
import { GoogleKeepConverter } from './GoogleKeepConverter'
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { Username } from '@standardnotes/snjs'
|
||||||
import { ChallengePrompt } from '@standardnotes/services'
|
import { ChallengePrompt } from '@standardnotes/services'
|
||||||
import { RefObject, useState } from 'react'
|
import { RefObject, useState } from 'react'
|
||||||
|
|
||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { isAndroid } from '@/Utils'
|
||||||
|
|
||||||
import Button from '../Button/Button'
|
import Button from '../Button/Button'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
|
|
||||||
import { InputValue } from './InputValue'
|
import { InputValue } from './InputValue'
|
||||||
import U2FPromptIframeContainer from './U2FPromptIframeContainer'
|
import U2FPromptIframeContainer from './U2FPromptIframeContainer'
|
||||||
|
|
||||||
@@ -18,7 +23,7 @@ const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData
|
|||||||
const [authenticatorResponse, setAuthenticatorResponse] = useState<Record<string, unknown> | null>(null)
|
const [authenticatorResponse, setAuthenticatorResponse] = useState<Record<string, unknown> | null>(null)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
if (!application.isFullU2FClient) {
|
if (!application.isFullU2FClient && !isAndroid()) {
|
||||||
return (
|
return (
|
||||||
<U2FPromptIframeContainer
|
<U2FPromptIframeContainer
|
||||||
contextData={contextData}
|
contextData={contextData}
|
||||||
@@ -28,48 +33,71 @@ const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="min-w-76">
|
||||||
|
{error && <div className="text-red-500">{error}</div>}
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
fullWidth
|
||||||
|
colorStyle={authenticatorResponse ? 'success' : 'info'}
|
||||||
|
onClick={async () => {
|
||||||
|
const usernameOrError = Username.create((contextData as { username: string }).username)
|
||||||
|
if (usernameOrError.isFailed()) {
|
||||||
|
setError(usernameOrError.getError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const username = usernameOrError.getValue()
|
||||||
|
|
||||||
|
let authenticatorResponse: Record<string, unknown> | null = null
|
||||||
|
if (isAndroid()) {
|
||||||
|
const authenticatorOptionsOrError = await application.getAuthenticatorAuthenticationOptions.execute({
|
||||||
|
username: username.value,
|
||||||
|
})
|
||||||
|
if (authenticatorOptionsOrError.isFailed()) {
|
||||||
|
setError(authenticatorOptionsOrError.getError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const authenticatorOptions = authenticatorOptionsOrError.getValue()
|
||||||
|
|
||||||
|
authenticatorResponse = await application
|
||||||
|
.mobileDevice()
|
||||||
|
.authenticateWithU2F(JSON.stringify(authenticatorOptions))
|
||||||
|
} else {
|
||||||
|
const authenticatorResponseOrError = await application.getAuthenticatorAuthenticationResponse.execute({
|
||||||
|
username: username.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (authenticatorResponseOrError.isFailed()) {
|
||||||
|
setError(authenticatorResponseOrError.getError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticatorResponse = authenticatorResponseOrError.getValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authenticatorResponse === null) {
|
||||||
|
setError('Failed to obtain device response')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthenticatorResponse(authenticatorResponse)
|
||||||
|
onValueChange(authenticatorResponse, prompt)
|
||||||
|
}}
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
{authenticatorResponse ? (
|
||||||
|
<span className="flex items-center justify-center gap-3">
|
||||||
|
<Icon type="check-circle" />
|
||||||
|
Obtained Device Response
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'Authenticate Device'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-w-76">
|
|
||||||
{error && <div className="text-red-500">{error}</div>}
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
fullWidth
|
|
||||||
colorStyle={authenticatorResponse ? 'success' : 'info'}
|
|
||||||
onClick={async () => {
|
|
||||||
if (!contextData || contextData.username === undefined) {
|
|
||||||
setError('No username provided')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const authenticatorResponseOrError = await application.getAuthenticatorAuthenticationResponse.execute({
|
|
||||||
username: contextData.username as string,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (authenticatorResponseOrError.isFailed()) {
|
|
||||||
setError(authenticatorResponseOrError.getError())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const authenticatorResponse = authenticatorResponseOrError.getValue()
|
|
||||||
|
|
||||||
setAuthenticatorResponse(authenticatorResponse)
|
|
||||||
onValueChange(authenticatorResponse, prompt)
|
|
||||||
}}
|
|
||||||
ref={buttonRef}
|
|
||||||
>
|
|
||||||
{authenticatorResponse ? (
|
|
||||||
<span className="flex items-center justify-center gap-3">
|
|
||||||
<Icon type="check-circle" />
|
|
||||||
Obtained Device Response
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
'Authenticate Device'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default U2FPrompt
|
export default U2FPrompt
|
||||||
|
|||||||
17
yarn.lock
17
yarn.lock
@@ -4780,7 +4780,7 @@ __metadata:
|
|||||||
resolution: "@standardnotes/api@workspace:packages/api"
|
resolution: "@standardnotes/api@workspace:packages/api"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common": ^1.46.6
|
"@standardnotes/common": ^1.46.6
|
||||||
"@standardnotes/domain-core": ^1.11.3
|
"@standardnotes/domain-core": ^1.12.0
|
||||||
"@standardnotes/encryption": "workspace:*"
|
"@standardnotes/encryption": "workspace:*"
|
||||||
"@standardnotes/models": "workspace:*"
|
"@standardnotes/models": "workspace:*"
|
||||||
"@standardnotes/responses": "workspace:*"
|
"@standardnotes/responses": "workspace:*"
|
||||||
@@ -5009,6 +5009,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@standardnotes/domain-core@npm:^1.12.0":
|
||||||
|
version: 1.12.0
|
||||||
|
resolution: "@standardnotes/domain-core@npm:1.12.0"
|
||||||
|
dependencies:
|
||||||
|
uuid: ^9.0.0
|
||||||
|
checksum: 852f15e481546fac621d503ea1cbdf9b2cb3e343d0c4cbe2fea7acc0f751a122d64479b55fa18e032e51d277a56621b9401d6f2d01756eb877eeb3572108e2a6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@standardnotes/domain-events@npm:^2.108.1":
|
"@standardnotes/domain-events@npm:^2.108.1":
|
||||||
version: 2.108.1
|
version: 2.108.1
|
||||||
resolution: "@standardnotes/domain-events@npm:2.108.1"
|
resolution: "@standardnotes/domain-events@npm:2.108.1"
|
||||||
@@ -5086,7 +5095,7 @@ __metadata:
|
|||||||
resolution: "@standardnotes/features@workspace:packages/features"
|
resolution: "@standardnotes/features@workspace:packages/features"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common": ^1.46.6
|
"@standardnotes/common": ^1.46.6
|
||||||
"@standardnotes/domain-core": ^1.11.3
|
"@standardnotes/domain-core": ^1.12.0
|
||||||
"@standardnotes/security": ^1.7.6
|
"@standardnotes/security": ^1.7.6
|
||||||
"@types/jest": ^29.2.3
|
"@types/jest": ^29.2.3
|
||||||
"@typescript-eslint/eslint-plugin": "*"
|
"@typescript-eslint/eslint-plugin": "*"
|
||||||
@@ -5397,7 +5406,7 @@ __metadata:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/api": "workspace:^"
|
"@standardnotes/api": "workspace:^"
|
||||||
"@standardnotes/common": ^1.46.4
|
"@standardnotes/common": ^1.46.4
|
||||||
"@standardnotes/domain-core": ^1.11.3
|
"@standardnotes/domain-core": ^1.12.0
|
||||||
"@standardnotes/encryption": "workspace:^"
|
"@standardnotes/encryption": "workspace:^"
|
||||||
"@standardnotes/files": "workspace:^"
|
"@standardnotes/files": "workspace:^"
|
||||||
"@standardnotes/models": "workspace:^"
|
"@standardnotes/models": "workspace:^"
|
||||||
@@ -5495,7 +5504,7 @@ __metadata:
|
|||||||
"@babel/preset-env": "*"
|
"@babel/preset-env": "*"
|
||||||
"@standardnotes/api": "workspace:*"
|
"@standardnotes/api": "workspace:*"
|
||||||
"@standardnotes/common": ^1.46.6
|
"@standardnotes/common": ^1.46.6
|
||||||
"@standardnotes/domain-core": ^1.11.3
|
"@standardnotes/domain-core": ^1.12.0
|
||||||
"@standardnotes/domain-events": ^2.108.1
|
"@standardnotes/domain-events": ^2.108.1
|
||||||
"@standardnotes/encryption": "workspace:*"
|
"@standardnotes/encryption": "workspace:*"
|
||||||
"@standardnotes/features": "workspace:*"
|
"@standardnotes/features": "workspace:*"
|
||||||
|
|||||||
Reference in New Issue
Block a user