First commit
This commit is contained in:
7
packages/core/analytics/package.json
Normal file
7
packages/core/analytics/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@core/analytics",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"nx": { "tags": ["scope:core","type:infra"] }
|
||||
}
|
||||
1
packages/core/analytics/src/index.ts
Normal file
1
packages/core/analytics/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const AnalyticsCore = {};
|
||||
7
packages/core/bff-client/package.json
Normal file
7
packages/core/bff-client/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@core/bff-client",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"nx": { "tags": ["scope:core","type:infra"] }
|
||||
}
|
||||
1
packages/core/bff-client/src/index.ts
Normal file
1
packages/core/bff-client/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const BFFClient = {};
|
||||
7
packages/core/consent/package.json
Normal file
7
packages/core/consent/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@core/consent",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"nx": { "tags": ["scope:core","type:infra"] }
|
||||
}
|
||||
1
packages/core/consent/src/index.ts
Normal file
1
packages/core/consent/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ConsentCore = {};
|
||||
7
packages/core/policy-client/package.json
Normal file
7
packages/core/policy-client/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@core/policy-client",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"nx": { "tags": ["scope:core","type:infra"] }
|
||||
}
|
||||
1
packages/core/policy-client/src/index.ts
Normal file
1
packages/core/policy-client/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PolicyClient = {};
|
||||
7
packages/core/runtime/package.json
Normal file
7
packages/core/runtime/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@core/runtime",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"nx": { "tags": ["scope:core","type:infra"] }
|
||||
}
|
||||
1
packages/core/runtime/src/index.ts
Normal file
1
packages/core/runtime/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const RuntimeCore = {};
|
||||
24
packages/core/security/package.json
Normal file
24
packages/core/security/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@core/security",
|
||||
"version": "1.0.0",
|
||||
"description": "Core security SDK for hardware-backed cryptography, DPoP, and contextual encryption",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "nx build",
|
||||
"test": "nx test",
|
||||
"lint": "nx lint",
|
||||
"typecheck": "nx typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-native-keychain": "^8.2.0",
|
||||
"react-native-crypto-js": "^1.0.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.7"
|
||||
},
|
||||
"nx": {
|
||||
"tags": ["scope:core", "type:infra", "platform:rn"]
|
||||
}
|
||||
}
|
||||
144
packages/core/security/src/DPoPService.ts
Normal file
144
packages/core/security/src/DPoPService.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { DPoPProof } from './types';
|
||||
|
||||
// Note: This requires a custom Native Module (NativeCryptoModule) capable of
|
||||
// generating ECC keys in the Secure Enclave and signing data with them.
|
||||
// This is a placeholder implementation that would need native module integration.
|
||||
|
||||
const DPOP_KEY_ALIAS = 'lynkedup.dpop.key.v1';
|
||||
|
||||
/**
|
||||
* DPoP (Demonstrating Proof-of-Possession) Service
|
||||
* Implements RFC 9449 for sender-constrained access tokens
|
||||
*
|
||||
* Features:
|
||||
* - Hardware-backed ECC key generation
|
||||
* - JWT signing with private key in secure enclave
|
||||
* - Request binding via HTTP method and URI
|
||||
*/
|
||||
export class DPoPService {
|
||||
private publicKey: string | null = null;
|
||||
|
||||
/**
|
||||
* Initialize DPoP key pair in secure enclave
|
||||
* Returns the public key JWK
|
||||
*/
|
||||
async initializeKeyPair(): Promise<string> {
|
||||
if (this.publicKey) return this.publicKey;
|
||||
|
||||
// Check if key exists natively using the alias, otherwise generate it
|
||||
// This would call into a native module
|
||||
let pubKey = await this.getPublicKeyFromNative(DPOP_KEY_ALIAS);
|
||||
|
||||
if (!pubKey) {
|
||||
pubKey = await this.generateHardwareECCKeyPair(DPOP_KEY_ALIAS);
|
||||
}
|
||||
|
||||
this.publicKey = pubKey;
|
||||
return pubKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a DPoP proof for HTTP request
|
||||
* @param method HTTP method (GET, POST, etc.)
|
||||
* @param uri Full request URI
|
||||
* @param accessToken Optional access token to bind to request
|
||||
*/
|
||||
async signProof(method: string, uri: string, accessToken?: string): Promise<string> {
|
||||
if (!this.publicKey) {
|
||||
await this.initializeKeyPair();
|
||||
}
|
||||
|
||||
// Construct the DPoP JWT payload (htu, htm, jti, ath)
|
||||
const payload = {
|
||||
htm: method, // HTTP Method
|
||||
htu: uri, // HTTP URI
|
||||
jti: uuidv4(), // Unique identifier for this proof
|
||||
iat: Math.floor(Date.now() / 1000), // Issued at time
|
||||
// Include hash of access token (ath) if present
|
||||
ath: accessToken ? this.base64url(this.sha256(accessToken)) : undefined,
|
||||
};
|
||||
|
||||
// Sign the JWT using the hardware-backed private key via the native module
|
||||
const signedJwt = await this.signJWTWithNative(payload, DPOP_KEY_ALIAS);
|
||||
|
||||
return signedJwt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign arbitrary data with DPoP key
|
||||
*/
|
||||
async signData(data: string): Promise<string> {
|
||||
if (!this.publicKey) {
|
||||
await this.initializeKeyPair();
|
||||
}
|
||||
|
||||
const signaturePayload = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
nonce: uuidv4()
|
||||
};
|
||||
|
||||
return this.signJWTWithNative(signaturePayload, DPOP_KEY_ALIAS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current public key
|
||||
*/
|
||||
getPublicKey(): string | null {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
// Native module integration methods (would be implemented via bridge)
|
||||
private async getPublicKeyFromNative(keyAlias: string): Promise<string | null> {
|
||||
// This would call into a native module
|
||||
// Example: return NativeCryptoModule.getPublicKey(keyAlias);
|
||||
console.warn('Native module integration required for getPublicKeyFromNative');
|
||||
return null;
|
||||
}
|
||||
|
||||
private async generateHardwareECCKeyPair(keyAlias: string): Promise<string> {
|
||||
// This would call into a native module to generate ECC key in secure enclave
|
||||
// Example: return NativeCryptoModule.generateHardwareECCKeyPair(keyAlias);
|
||||
console.warn('Native module integration required for generateHardwareECCKeyPair');
|
||||
|
||||
// Mock implementation for development
|
||||
return JSON.stringify({
|
||||
kty: 'EC',
|
||||
crv: 'P-256',
|
||||
x: 'mock-x-coordinate',
|
||||
y: 'mock-y-coordinate',
|
||||
use: 'sig',
|
||||
kid: keyAlias
|
||||
});
|
||||
}
|
||||
|
||||
private async signJWTWithNative(payload: any, keyAlias: string): Promise<string> {
|
||||
// This would call into a native module to sign JWT with hardware key
|
||||
// Example: return NativeCryptoModule.signJWT(payload, keyAlias);
|
||||
console.warn('Native module integration required for signJWTWithNative');
|
||||
|
||||
// Mock implementation for development
|
||||
const header = { alg: 'ES256', typ: 'dpop+jwt', jwk: JSON.parse(this.publicKey!) };
|
||||
const encodedHeader = this.base64url(JSON.stringify(header));
|
||||
const encodedPayload = this.base64url(JSON.stringify(payload));
|
||||
const signature = 'mock-signature'; // Would be actual signature from secure enclave
|
||||
|
||||
return `${encodedHeader}.${encodedPayload}.${signature}`;
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
private base64url(str: string): string {
|
||||
return Buffer.from(str)
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
}
|
||||
|
||||
private sha256(str: string): string {
|
||||
// This would use a proper crypto library
|
||||
console.warn('Proper SHA256 implementation required');
|
||||
return 'mock-hash';
|
||||
}
|
||||
}
|
||||
165
packages/core/security/src/SecurityCore.ts
Normal file
165
packages/core/security/src/SecurityCore.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { DPoPService } from './DPoPService';
|
||||
import { CryptoService } from './CryptoService';
|
||||
import { KeyManagementService } from './KeyManagementService';
|
||||
import type { SecurityConfig, EncryptionResult, KeyReference } from './types';
|
||||
|
||||
/**
|
||||
* Core Security SDK
|
||||
* Provides hardware-backed cryptography, DPoP implementation, and contextual encryption
|
||||
*
|
||||
* Key Features (per FRD):
|
||||
* - F.SC.001: Cryptographic utilities (AES-GCM envelope encryption)
|
||||
* - F.SC.002: Hardware-backed key storage (Keychain/StrongBox)
|
||||
* - F.SC.003: Certificate Pinning logic
|
||||
* - F.SC.004: DPoP proof generation
|
||||
* - F.SC.005: Organizational Data Encryption Keys (DEKs) management
|
||||
* - F.SC.006: Secure wipe/cryptographic erasure
|
||||
* - F.SC.007: Device attestation integration
|
||||
*/
|
||||
export class SecurityCore {
|
||||
private config: SecurityConfig;
|
||||
private dpopService: DPoPService;
|
||||
private cryptoService: CryptoService;
|
||||
private keyManagement: KeyManagementService;
|
||||
private initialized = false;
|
||||
|
||||
constructor(config: SecurityConfig) {
|
||||
this.config = config;
|
||||
this.dpopService = new DPoPService();
|
||||
this.cryptoService = new CryptoService();
|
||||
this.keyManagement = new KeyManagementService(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Security Core
|
||||
* Must be called before any other operations
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
await this.keyManagement.initialize();
|
||||
await this.dpopService.initializeKeyPair();
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* F.SC.004: Generate DPoP proof for request authentication
|
||||
*/
|
||||
async signDPoPProof(method: string, uri: string, accessToken?: string): Promise<string> {
|
||||
this.ensureInitialized();
|
||||
return this.dpopService.signProof(method, uri, accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* F.SC.001: Encrypt data using organizational DEK (envelope encryption)
|
||||
*/
|
||||
async encryptForOrganization(data: string, orgId: string): Promise<EncryptionResult> {
|
||||
this.ensureInitialized();
|
||||
|
||||
// Get or create organizational DEK
|
||||
const orgKeyRef = await this.keyManagement.getOrganizationalKey(orgId);
|
||||
|
||||
// Generate random file encryption key
|
||||
const fileKey = await this.cryptoService.generateKey();
|
||||
|
||||
// Encrypt data with file key
|
||||
const encryptedData = await this.cryptoService.encrypt(data, fileKey);
|
||||
|
||||
// Wrap file key with organizational DEK
|
||||
const wrappedKey = await this.cryptoService.wrapKey(fileKey, orgKeyRef.keyId);
|
||||
|
||||
return {
|
||||
encryptedData: encryptedData.ciphertext,
|
||||
keyReference: {
|
||||
keyId: wrappedKey,
|
||||
orgId,
|
||||
keyType: 'dek',
|
||||
createdAt: new Date().toISOString()
|
||||
},
|
||||
iv: encryptedData.iv,
|
||||
authTag: encryptedData.authTag
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt data using organizational DEK
|
||||
*/
|
||||
async decryptForOrganization(encryptedData: string, keyReference: KeyReference): Promise<string> {
|
||||
this.ensureInitialized();
|
||||
|
||||
if (!keyReference.orgId) {
|
||||
throw new Error('Organization ID required for decryption');
|
||||
}
|
||||
|
||||
// Get organizational DEK
|
||||
const orgKeyRef = await this.keyManagement.getOrganizationalKey(keyReference.orgId);
|
||||
|
||||
// Unwrap file key
|
||||
const fileKey = await this.cryptoService.unwrapKey(keyReference.keyId, orgKeyRef.keyId);
|
||||
|
||||
// Decrypt data
|
||||
return this.cryptoService.decrypt(encryptedData, fileKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* F.SC.002: Get database master key from secure storage
|
||||
*/
|
||||
async getDatabaseMasterKey(): Promise<string> {
|
||||
this.ensureInitialized();
|
||||
return this.keyManagement.getDatabaseMasterKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* F.SC.005: Store authentication tokens securely
|
||||
*/
|
||||
async storeAuthTokens(accessToken: string, refreshToken: string): Promise<void> {
|
||||
this.ensureInitialized();
|
||||
await this.keyManagement.storeSecureValue('auth.access_token', accessToken);
|
||||
await this.keyManagement.storeSecureValue('auth.refresh_token', refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored authentication tokens
|
||||
*/
|
||||
async getAuthTokens(): Promise<{ accessToken?: string; refreshToken?: string }> {
|
||||
this.ensureInitialized();
|
||||
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
this.keyManagement.getSecureValue('auth.access_token'),
|
||||
this.keyManagement.getSecureValue('auth.refresh_token')
|
||||
]);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
/**
|
||||
* F.SC.006: Cryptographic erasure - delete organizational keys
|
||||
*/
|
||||
async performCryptographicErasure(orgId: string): Promise<void> {
|
||||
this.ensureInitialized();
|
||||
await this.keyManagement.deleteOrganizationalKeys(orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* F.SC.006: Complete secure wipe - delete all keys
|
||||
*/
|
||||
async performSecureWipe(): Promise<void> {
|
||||
this.ensureInitialized();
|
||||
await this.keyManagement.secureWipe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign arbitrary data with identity key
|
||||
*/
|
||||
async signData(data: string): Promise<string> {
|
||||
this.ensureInitialized();
|
||||
return this.dpopService.signData(data);
|
||||
}
|
||||
|
||||
private ensureInitialized(): void {
|
||||
if (!this.initialized) {
|
||||
throw new Error('SecurityCore must be initialized before use');
|
||||
}
|
||||
}
|
||||
}
|
||||
11
packages/core/security/src/index.ts
Normal file
11
packages/core/security/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export { SecurityCore } from './SecurityCore';
|
||||
export { DPoPService } from './DPoPService';
|
||||
export { CryptoService } from './CryptoService';
|
||||
export { KeyManagementService } from './KeyManagementService';
|
||||
export type {
|
||||
SecurityConfig,
|
||||
DPoPProof,
|
||||
EncryptionResult,
|
||||
KeyReference,
|
||||
SecurityLevel
|
||||
} from './types';
|
||||
39
packages/core/security/src/types.ts
Normal file
39
packages/core/security/src/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export interface SecurityConfig {
|
||||
enableHardwareBackedStorage: boolean;
|
||||
requireSecureEnclave: boolean;
|
||||
enableCertificatePinning: boolean;
|
||||
allowDebugging: boolean;
|
||||
}
|
||||
|
||||
export interface DPoPProof {
|
||||
jwt: string;
|
||||
publicKey: string;
|
||||
algorithm: string;
|
||||
}
|
||||
|
||||
export interface EncryptionResult {
|
||||
encryptedData: string;
|
||||
keyReference: KeyReference;
|
||||
iv: string;
|
||||
authTag: string;
|
||||
}
|
||||
|
||||
export interface KeyReference {
|
||||
keyId: string;
|
||||
orgId?: string;
|
||||
keyType: 'master' | 'dek' | 'dpop' | 'identity';
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export enum SecurityLevel {
|
||||
SOFTWARE = 'software',
|
||||
SECURE_HARDWARE = 'secure_hardware',
|
||||
SECURE_ENCLAVE = 'secure_enclave'
|
||||
}
|
||||
|
||||
export interface AttestationResult {
|
||||
status: 'VALID' | 'INVALID' | 'UNKNOWN';
|
||||
deviceCheck?: any;
|
||||
playIntegrity?: any;
|
||||
timestamp: string;
|
||||
}
|
||||
26
packages/core/storage/package.json
Normal file
26
packages/core/storage/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@core/storage",
|
||||
"version": "1.0.0",
|
||||
"description": "Core storage SDK for encrypted RxDB/SQLCipher database management",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "nx build",
|
||||
"test": "nx test",
|
||||
"lint": "nx lint",
|
||||
"typecheck": "nx typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"rxdb": "^15.0.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@core/security": "workspace:*"
|
||||
},
|
||||
"nx": {
|
||||
"tags": ["scope:core", "type:infra", "platform:rn"]
|
||||
}
|
||||
}
|
||||
220
packages/core/storage/src/StorageCore.ts
Normal file
220
packages/core/storage/src/StorageCore.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import type { SecurityCore } from '@core/security';
|
||||
import { DatabaseProvider } from './DatabaseProvider';
|
||||
import { SchemaRegistry } from './SchemaRegistry';
|
||||
import type { StorageConfig, CollectionSchema, DatabaseInstance } from './types';
|
||||
|
||||
/**
|
||||
* Core Storage SDK
|
||||
* Provides encrypted local database management with RxDB/SQLCipher
|
||||
*
|
||||
* Key Features (per FRD):
|
||||
* - F.STC.001: Abstraction layer over RxDB/SQLite with SQLCipher encryption
|
||||
* - F.STC.002: Schema registration for feature SDKs
|
||||
* - F.STC.003: Conflict resolution strategies (CRDT configuration, LWW handling)
|
||||
* - F.STC.004: Encrypted file vault for local media storage
|
||||
* - F.STC.005: Automated database schema migration
|
||||
* - F.STC.006: Incremental encryption re-keying
|
||||
* - F.STC.007: Selective purge mechanisms
|
||||
*/
|
||||
export class StorageCore {
|
||||
private config: StorageConfig;
|
||||
private securityCore: SecurityCore;
|
||||
private databaseProvider: DatabaseProvider;
|
||||
private schemaRegistry: SchemaRegistry;
|
||||
private initialized = false;
|
||||
|
||||
constructor(config: StorageConfig, securityCore: SecurityCore) {
|
||||
this.config = config;
|
||||
this.securityCore = securityCore;
|
||||
this.databaseProvider = new DatabaseProvider(config, securityCore);
|
||||
this.schemaRegistry = new SchemaRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* F.STC.001: Initialize the storage layer with encryption
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
await this.databaseProvider.initialize();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* F.STC.002: Register schema for feature SDKs
|
||||
*/
|
||||
registerSchema(collectionName: string, schema: CollectionSchema): void {
|
||||
this.schemaRegistry.register(collectionName, schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database instance
|
||||
*/
|
||||
getDatabase(): DatabaseInstance {
|
||||
this.ensureInitialized();
|
||||
return this.databaseProvider.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* F.STC.002: Create collection with registered schema
|
||||
*/
|
||||
async createCollection(name: string): Promise<void> {
|
||||
this.ensureInitialized();
|
||||
|
||||
const schema = this.schemaRegistry.getSchema(name);
|
||||
if (!schema) {
|
||||
throw new Error(`Schema not registered for collection: ${name}`);
|
||||
}
|
||||
|
||||
await this.databaseProvider.createCollection(name, schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* F.STC.004: Store encrypted file
|
||||
*/
|
||||
async storeEncryptedFile(
|
||||
filename: string,
|
||||
data: Buffer,
|
||||
mimeType: string,
|
||||
orgId?: string
|
||||
): Promise<string> {
|
||||
this.ensureInitialized();
|
||||
|
||||
// Encrypt file content
|
||||
const encryptResult = orgId
|
||||
? await this.securityCore.encryptForOrganization(data.toString('base64'), orgId)
|
||||
: await this.encryptFileLocally(data);
|
||||
|
||||
// Store encrypted file to filesystem
|
||||
const encryptedPath = await this.writeEncryptedFile(filename, encryptResult.encryptedData);
|
||||
|
||||
// Store metadata in database
|
||||
const fileId = this.generateFileId();
|
||||
const metadata = {
|
||||
id: fileId,
|
||||
filename,
|
||||
mimeType,
|
||||
size: data.length,
|
||||
encryptedPath,
|
||||
keyReference: JSON.stringify(encryptResult.keyReference),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const db = this.getDatabase();
|
||||
await db.collections.fileVault.insert(metadata);
|
||||
|
||||
return fileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* F.STC.004: Retrieve encrypted file
|
||||
*/
|
||||
async retrieveEncryptedFile(fileId: string, orgId?: string): Promise<Buffer> {
|
||||
this.ensureInitialized();
|
||||
|
||||
const db = this.getDatabase();
|
||||
const metadata = await db.collections.fileVault.findOne(fileId).exec();
|
||||
|
||||
if (!metadata) {
|
||||
throw new Error(`File not found: ${fileId}`);
|
||||
}
|
||||
|
||||
// Read encrypted file
|
||||
const encryptedData = await this.readEncryptedFile(metadata.encryptedPath);
|
||||
const keyReference = JSON.parse(metadata.keyReference);
|
||||
|
||||
// Decrypt file content
|
||||
const decryptedData = orgId
|
||||
? await this.securityCore.decryptForOrganization(encryptedData, keyReference)
|
||||
: await this.decryptFileLocally(encryptedData, keyReference);
|
||||
|
||||
return Buffer.from(decryptedData, 'base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* F.STC.007: Selective purge by organization
|
||||
*/
|
||||
async purgeOrganizationData(orgId: string): Promise<void> {
|
||||
this.ensureInitialized();
|
||||
|
||||
const db = this.getDatabase();
|
||||
|
||||
// Find all collections and purge org-specific data
|
||||
for (const [collectionName, collection] of Object.entries(db.collections)) {
|
||||
// Remove documents with matching orgId
|
||||
await collection.find({ orgId }).remove();
|
||||
}
|
||||
|
||||
// Purge encrypted files belonging to organization
|
||||
const orgFiles = await db.collections.fileVault
|
||||
.find()
|
||||
.where('keyReference')
|
||||
.regex(new RegExp(orgId))
|
||||
.exec();
|
||||
|
||||
for (const file of orgFiles) {
|
||||
await this.deleteEncryptedFile(file.encryptedPath);
|
||||
await file.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* F.STC.006: Re-encrypt data with new keys
|
||||
*/
|
||||
async reEncryptData(orgId: string, newKeyReference: any): Promise<void> {
|
||||
this.ensureInitialized();
|
||||
|
||||
// This would implement re-encryption of existing data
|
||||
// when organizational keys are rotated
|
||||
console.warn('Re-encryption implementation needed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sync status for collections
|
||||
*/
|
||||
getSyncStatus(collectionName: string): any {
|
||||
this.ensureInitialized();
|
||||
|
||||
const db = this.getDatabase();
|
||||
return db.collections[collectionName]?.find({ syncStatus: 'PENDING' });
|
||||
}
|
||||
|
||||
private async encryptFileLocally(data: Buffer): Promise<any> {
|
||||
// Implementation would use local encryption for non-org files
|
||||
throw new Error('Local file encryption not implemented');
|
||||
}
|
||||
|
||||
private async decryptFileLocally(encryptedData: string, keyReference: any): Promise<string> {
|
||||
// Implementation would use local decryption for non-org files
|
||||
throw new Error('Local file decryption not implemented');
|
||||
}
|
||||
|
||||
private async writeEncryptedFile(filename: string, encryptedData: string): Promise<string> {
|
||||
// Implementation would write to secure app documents directory
|
||||
const path = `/secure/${this.generateFileId()}_${filename}`;
|
||||
console.warn('File system integration needed');
|
||||
return path;
|
||||
}
|
||||
|
||||
private async readEncryptedFile(path: string): Promise<string> {
|
||||
// Implementation would read from secure app documents directory
|
||||
console.warn('File system integration needed');
|
||||
return 'mock-encrypted-data';
|
||||
}
|
||||
|
||||
private async deleteEncryptedFile(path: string): Promise<void> {
|
||||
// Implementation would securely delete file
|
||||
console.warn('Secure file deletion needed');
|
||||
}
|
||||
|
||||
private generateFileId(): string {
|
||||
return `file_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
||||
}
|
||||
|
||||
private ensureInitialized(): void {
|
||||
if (!this.initialized) {
|
||||
throw new Error('StorageCore must be initialized before use');
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/core/storage/src/index.ts
Normal file
9
packages/core/storage/src/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { StorageCore } from './StorageCore';
|
||||
export { DatabaseProvider } from './DatabaseProvider';
|
||||
export { SchemaRegistry } from './SchemaRegistry';
|
||||
export type {
|
||||
StorageConfig,
|
||||
CollectionSchema,
|
||||
MigrationStrategy,
|
||||
ConflictResolutionStrategy
|
||||
} from './types';
|
||||
42
packages/core/storage/src/types.ts
Normal file
42
packages/core/storage/src/types.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { RxDatabase, RxCollection } from 'rxdb';
|
||||
|
||||
export interface StorageConfig {
|
||||
databaseName: string;
|
||||
enableEncryption: boolean;
|
||||
enableCRDT: boolean;
|
||||
migrationStrategy: 'drop' | 'migrate';
|
||||
}
|
||||
|
||||
export interface CollectionSchema {
|
||||
version: number;
|
||||
title: string;
|
||||
type: 'object';
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
indexes?: string[];
|
||||
migrationStrategies?: Record<number, MigrationStrategy>;
|
||||
conflictResolution?: ConflictResolutionStrategy;
|
||||
}
|
||||
|
||||
export type MigrationStrategy = (oldDoc: any) => any;
|
||||
|
||||
export interface ConflictResolutionStrategy {
|
||||
type: 'crdt' | 'lww' | 'custom';
|
||||
resolver?: (conflicts: any[]) => any;
|
||||
}
|
||||
|
||||
export interface DatabaseInstance {
|
||||
database: RxDatabase;
|
||||
collections: Record<string, RxCollection>;
|
||||
}
|
||||
|
||||
export interface EncryptedFileMetadata {
|
||||
id: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
encryptedPath: string;
|
||||
keyReference: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
7
packages/core/sync/package.json
Normal file
7
packages/core/sync/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@core/sync",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"nx": { "tags": ["scope:core","type:infra"] }
|
||||
}
|
||||
3
packages/core/sync/src/index.ts
Normal file
3
packages/core/sync/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const SyncCore = {
|
||||
// placeholder sync core
|
||||
};
|
||||
7
packages/core/trust/package.json
Normal file
7
packages/core/trust/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "@core/trust",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"nx": { "tags": ["scope:core","type:infra"] }
|
||||
}
|
||||
1
packages/core/trust/src/index.ts
Normal file
1
packages/core/trust/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const TrustCore = {};
|
||||
279
packages/feature-auth/README.playbook.md
Normal file
279
packages/feature-auth/README.playbook.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# @feature/auth Playbook
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Handles Authentication (OTP, Magic Link, Federated), Session Management, and Biometrics with risk-adaptive security.
|
||||
|
||||
**Key Features (per FRD):**
|
||||
- F.ID.001: Login, Registration, and Forgot Password flows
|
||||
- F.ID.002: Configurable verification (OTP, Email, Magic Links)
|
||||
- F.ID.003: Secure session token storage and rotation
|
||||
- F.ID.004: Device biometrics integration (FaceID/TouchID/Android Biometrics)
|
||||
- F.ID.005: Configurable "Remember Me" duration
|
||||
- F.ID.006: Enhanced Magic Link security with nonce and expiry
|
||||
- F.ID.007: Federated Login connectors (Google, Apple)
|
||||
|
||||
## 2. Setup & Dependencies
|
||||
|
||||
**Required Core SDKs:**
|
||||
- `@core/security` - DPoP key generation and signing
|
||||
- `@core/trust` - Runtime Risk Score; triggers Step-Up MFA if score is high
|
||||
- `@core/policy` - Risk-adaptive policy checks
|
||||
- `@core/storage` - Encrypted session storage
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
# Install auth feature layers
|
||||
pnpm add @feature/auth/domain @feature/auth/data @feature/auth/ui-rn
|
||||
|
||||
# Peer dependencies (automatically resolved in monorepo)
|
||||
# @core/security @core/trust @core/policy @core/storage
|
||||
```
|
||||
|
||||
## 3. Core Workflow: DPoP Login and Risk Assessment
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App
|
||||
participant Auth as @feature/auth
|
||||
participant Trust as @core/trust
|
||||
participant Security as @core/security
|
||||
participant Policy as @core/policy
|
||||
participant BFF
|
||||
|
||||
App->>Auth: login(credentials)
|
||||
Auth->>Trust: calculateRiskScore()
|
||||
Trust-->>Auth: { score: 45, signals: {...} }
|
||||
|
||||
Auth->>Security: initializeDPoP()
|
||||
Security-->>Auth: publicKey
|
||||
|
||||
Auth->>BFF: exchangeCredentials(creds, pubKey)
|
||||
BFF-->>Auth: session + tokens
|
||||
|
||||
alt Risk Score > Threshold
|
||||
Auth->>Policy: check('step-up-mfa', context)
|
||||
Policy-->>Auth: requires_verification: true
|
||||
Auth-->>App: { success: true, requiresStepUp: true }
|
||||
else Low Risk
|
||||
Auth-->>App: { success: true, session }
|
||||
end
|
||||
```
|
||||
|
||||
## 4. Integration Points
|
||||
|
||||
### @core/security Integration
|
||||
```typescript
|
||||
// DPoP key generation and signing
|
||||
const publicKey = await this.authRepository.initializeDPoP();
|
||||
const signedProof = await this.securityCore.signDPoPProof('POST', '/auth/login', accessToken);
|
||||
```
|
||||
|
||||
### @core/trust Integration
|
||||
```typescript
|
||||
// Risk assessment for adaptive authentication
|
||||
const { score, signals } = await this.trustRepository.calculateRiskScore();
|
||||
const requiresStepUp = this.shouldRequireStepUp(score, signals);
|
||||
```
|
||||
|
||||
### @core/policy Integration
|
||||
```typescript
|
||||
// Risk-adaptive policy checks
|
||||
const allowed = await this.policyClient.check('login', 'User', {
|
||||
riskScore: 60,
|
||||
deviceSignals: signals
|
||||
});
|
||||
```
|
||||
|
||||
## 5. Policy Enforcement Examples
|
||||
|
||||
**Risk-Adaptive Login:**
|
||||
```typescript
|
||||
// Low risk (score < 50): Standard login
|
||||
await policyClient.check('login', 'User', { riskScore: 35 }); // → true
|
||||
|
||||
// Medium risk (50-75): Email verification
|
||||
await policyClient.check('login', 'User', { riskScore: 65 }); // → requires email MFA
|
||||
|
||||
// High risk (75+): OTP + Device attestation
|
||||
await policyClient.check('login', 'User', { riskScore: 85 }); // → requires OTP + attestation
|
||||
```
|
||||
|
||||
**Organization Access:**
|
||||
```typescript
|
||||
// Check organization membership with role-based permissions
|
||||
await policyClient.check('access-org', 'Organization', {
|
||||
orgId: 'org-123',
|
||||
userRole: 'member',
|
||||
riskScore: 40
|
||||
});
|
||||
```
|
||||
|
||||
## 6. API Usage Examples
|
||||
|
||||
### Basic Login Flow
|
||||
```typescript
|
||||
import { LoginUseCase } from '@feature/auth/domain';
|
||||
import { AuthRepository } from '@feature/auth/data';
|
||||
|
||||
const authRepo = new AuthRepository(securityCore, bffClient);
|
||||
const trustRepo = new TrustRepository(trustCore);
|
||||
const loginUseCase = new LoginUseCase(authRepo, trustRepo, config);
|
||||
|
||||
const result = await loginUseCase.execute({
|
||||
identifier: 'user@example.com',
|
||||
password: 'secure_password',
|
||||
deviceId: 'device-uuid'
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
if (result.requiresStepUp) {
|
||||
// Handle step-up MFA flow
|
||||
console.log('Additional verification required:', result.verificationMethod);
|
||||
} else {
|
||||
// Login successful
|
||||
console.log('User session:', result.session);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Biometric Authentication
|
||||
```typescript
|
||||
import { BiometricUseCase } from '@feature/auth/domain';
|
||||
|
||||
const biometricUseCase = new BiometricUseCase(authRepo, biometricConfig);
|
||||
|
||||
const result = await biometricUseCase.authenticate({
|
||||
promptMessage: 'Authenticate to access LynkedUp',
|
||||
fallbackToPassword: true
|
||||
});
|
||||
```
|
||||
|
||||
### Magic Link Flow
|
||||
```typescript
|
||||
// Generate magic link
|
||||
const magicLink = await authRepo.generateMagicLink('user@example.com');
|
||||
|
||||
// Verify magic link (typically called from deep link handler)
|
||||
const result = await loginUseCase.executeWithMagicLink(token, nonce);
|
||||
```
|
||||
|
||||
## 7. Configuration
|
||||
|
||||
```typescript
|
||||
const authConfig: AuthConfig = {
|
||||
requireEmailVerification: true,
|
||||
requirePhoneVerification: false,
|
||||
biometrics: {
|
||||
enabled: true,
|
||||
fallbackToPassword: true,
|
||||
promptMessage: 'Authenticate with LynkedUp'
|
||||
},
|
||||
rememberMeDays: 30,
|
||||
maxLoginAttempts: 5,
|
||||
lockoutDurationMinutes: 15
|
||||
};
|
||||
```
|
||||
|
||||
## 8. Error Handling
|
||||
|
||||
```typescript
|
||||
// Standard error codes returned by AuthResult
|
||||
switch (result.errorCode) {
|
||||
case 'INVALID_CREDENTIALS':
|
||||
// Handle invalid login
|
||||
break;
|
||||
case 'ACCOUNT_LOCKED':
|
||||
// Handle account lockout
|
||||
break;
|
||||
case 'VERIFICATION_REQUIRED':
|
||||
// Handle unverified account
|
||||
break;
|
||||
case 'DEVICE_NOT_TRUSTED':
|
||||
// Handle untrusted device
|
||||
break;
|
||||
case 'POLICY_VIOLATION':
|
||||
// Handle policy-based denial
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
```typescript
|
||||
// Domain layer testing (LoginUseCase)
|
||||
describe('LoginUseCase', () => {
|
||||
it('should require step-up for high risk score', async () => {
|
||||
mockTrustRepo.calculateRiskScore.mockResolvedValue({ score: 85, signals: {} });
|
||||
|
||||
const result = await loginUseCase.execute(validCredentials);
|
||||
|
||||
expect(result.requiresStepUp).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Contract Tests
|
||||
```typescript
|
||||
// Data layer testing against mocked BFF
|
||||
import { setupServer } from 'msw/node';
|
||||
import { graphql } from 'msw';
|
||||
|
||||
const server = setupServer(
|
||||
graphql.mutation('Login', (req, res, ctx) => {
|
||||
return res(ctx.data({
|
||||
login: {
|
||||
accessToken: 'mock-token',
|
||||
user: mockUser
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## 10. Security Notes
|
||||
|
||||
**Critical Security Requirements:**
|
||||
- Private keys MUST remain in the Secure Enclave/TEE
|
||||
- Magic links MUST enforce nonce and expiry (see F.ID.006)
|
||||
- DPoP proofs MUST be bound to specific HTTP requests
|
||||
- Session tokens MUST be stored in hardware-backed keychain
|
||||
- Step-up MFA triggers MUST be policy-driven, not hardcoded
|
||||
|
||||
**Risk Signals Handling:**
|
||||
- Root/Jailbreak detection → Immediate step-up required
|
||||
- Device attestation failure → Block access + notify admin
|
||||
- Geolocation deviation → Email verification required
|
||||
- New device → SMS OTP required
|
||||
|
||||
## 11. Troubleshooting
|
||||
|
||||
**Common Issues:**
|
||||
|
||||
1. **"SecurityCore must be initialized"**
|
||||
- Ensure `securityCore.initialize()` is called before auth operations
|
||||
|
||||
2. **"DPoP key generation failed"**
|
||||
- Verify hardware-backed storage is available
|
||||
- Check device security settings (passcode/biometrics enabled)
|
||||
|
||||
3. **"Policy evaluation failed"**
|
||||
- Verify policy bundles are cached locally
|
||||
- Check network connectivity for policy updates
|
||||
|
||||
4. **Biometric authentication unavailable**
|
||||
- Verify device biometric enrollment
|
||||
- Check app permissions for biometric access
|
||||
|
||||
## 12. Performance Considerations
|
||||
|
||||
- **Key Operations**: DPoP key generation (one-time, ~100ms)
|
||||
- **Risk Assessment**: Device evaluation (~50-200ms depending on signals)
|
||||
- **Policy Evaluation**: Local cache lookup (~1-5ms)
|
||||
- **Session Storage**: Keychain operations (~10-50ms)
|
||||
|
||||
**Optimization Tips:**
|
||||
- Cache risk assessment results for 5-10 minutes
|
||||
- Pre-warm DPoP keys during app initialization
|
||||
- Batch policy evaluations when possible
|
||||
6
packages/feature-auth/data/package.json
Normal file
6
packages/feature-auth/data/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/auth/data",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-auth/data/src/index.ts
Normal file
1
packages/feature-auth/data/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const AuthData = {};
|
||||
22
packages/feature-auth/domain/package.json
Normal file
22
packages/feature-auth/domain/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@feature/auth/domain",
|
||||
"version": "1.0.0",
|
||||
"description": "Authentication domain logic - pure business rules and use cases",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "nx build",
|
||||
"test": "nx test",
|
||||
"lint": "nx lint",
|
||||
"typecheck": "nx typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.7"
|
||||
},
|
||||
"nx": {
|
||||
"tags": ["scope:feature", "type:domain", "platform:shared"]
|
||||
}
|
||||
}
|
||||
53
packages/feature-auth/domain/src/entities.ts
Normal file
53
packages/feature-auth/domain/src/entities.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
phoneNumber?: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isEmailVerified: boolean;
|
||||
isPhoneVerified: boolean;
|
||||
createdAt: string;
|
||||
lastLoginAt?: string;
|
||||
profilePictureUrl?: string;
|
||||
organizations: UserOrganization[];
|
||||
preferences: UserPreferences;
|
||||
}
|
||||
|
||||
export interface UserOrganization {
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
role: string;
|
||||
permissions: string[];
|
||||
joinedAt: string;
|
||||
status: 'active' | 'suspended' | 'pending';
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
language: string;
|
||||
timezone: string;
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
notifications: NotificationPreferences;
|
||||
}
|
||||
|
||||
export interface NotificationPreferences {
|
||||
email: boolean;
|
||||
push: boolean;
|
||||
sms: boolean;
|
||||
marketing: boolean;
|
||||
}
|
||||
|
||||
export interface AuthResult {
|
||||
success: boolean;
|
||||
session?: any;
|
||||
requiresVerification?: boolean;
|
||||
verificationMethod?: VerificationMethod;
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
nextSteps?: string[];
|
||||
}
|
||||
|
||||
export type VerificationMethod =
|
||||
| { type: 'otp'; target: string }
|
||||
| { type: 'email'; target: string }
|
||||
| { type: 'biometric' }
|
||||
| { type: 'magic_link'; target: string };
|
||||
20
packages/feature-auth/domain/src/index.ts
Normal file
20
packages/feature-auth/domain/src/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const AuthDomain = {};
|
||||
import { LoginUseCase } from './usecases/LoginUseCase';
|
||||
import { RegistrationUseCase } from './usecases/RegistrationUseCase';
|
||||
import { SessionUseCase } from './usecases/SessionUseCase';
|
||||
import { BiometricUseCase } from './usecases/BiometricUseCase';
|
||||
|
||||
export { LoginUseCase, RegistrationUseCase, SessionUseCase, BiometricUseCase };
|
||||
export type {
|
||||
IAuthRepository,
|
||||
ITrustRepository,
|
||||
Credentials,
|
||||
UserSession,
|
||||
BiometricConfig,
|
||||
AuthConfig
|
||||
} from './interfaces';
|
||||
export type {
|
||||
User,
|
||||
AuthResult,
|
||||
VerificationMethod
|
||||
} from './entities';
|
||||
76
packages/feature-auth/domain/src/interfaces.ts
Normal file
76
packages/feature-auth/domain/src/interfaces.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { User, AuthResult } from './entities';
|
||||
|
||||
/**
|
||||
* Repository interface that the data layer must implement
|
||||
* Following clean architecture principles - domain defines the contract
|
||||
*/
|
||||
export interface IAuthRepository {
|
||||
// F.ID.004: DPoP initialization
|
||||
initializeDPoP(): Promise<string>;
|
||||
|
||||
// F.ID.001: Login flows
|
||||
exchangeCredentialsForToken(creds: Credentials, pubKey: string): Promise<UserSession>;
|
||||
|
||||
// F.ID.002: Verification flows
|
||||
sendOTPVerification(phoneNumber: string): Promise<void>;
|
||||
verifyOTP(phoneNumber: string, code: string): Promise<boolean>;
|
||||
sendEmailVerification(email: string): Promise<void>;
|
||||
verifyEmail(email: string, token: string): Promise<boolean>;
|
||||
|
||||
// Magic Links (F.ID.006 - Enhanced security)
|
||||
generateMagicLink(email: string): Promise<string>;
|
||||
verifyMagicLink(token: string, nonce: string): Promise<UserSession>;
|
||||
|
||||
// F.ID.007: Federated login
|
||||
authenticateWithProvider(provider: 'google' | 'apple', token: string): Promise<UserSession>;
|
||||
|
||||
// Session management
|
||||
storeSession(session: UserSession): Promise<void>;
|
||||
getSession(): Promise<UserSession | null>;
|
||||
refreshSession(refreshToken: string): Promise<UserSession>;
|
||||
revokeSession(): Promise<void>;
|
||||
|
||||
// F.ID.005: Remember me functionality
|
||||
enableRememberMe(duration: number): Promise<void>;
|
||||
disableRememberMe(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trust/Risk assessment interface
|
||||
*/
|
||||
export interface ITrustRepository {
|
||||
calculateRiskScore(): Promise<{ score: number; signals: Record<string, any> }>;
|
||||
performDeviceAttestation(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface Credentials {
|
||||
identifier: string; // email or phone
|
||||
password?: string;
|
||||
biometricSignature?: string;
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export interface UserSession {
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: string;
|
||||
user: User;
|
||||
riskScore?: number;
|
||||
requiresStepUp?: boolean;
|
||||
}
|
||||
|
||||
export interface BiometricConfig {
|
||||
enabled: boolean;
|
||||
fallbackToPassword: boolean;
|
||||
promptMessage: string;
|
||||
}
|
||||
|
||||
export interface AuthConfig {
|
||||
requireEmailVerification: boolean;
|
||||
requirePhoneVerification: boolean;
|
||||
biometrics: BiometricConfig;
|
||||
rememberMeDays: number;
|
||||
maxLoginAttempts: number;
|
||||
lockoutDurationMinutes: number;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class BiometricUseCase {
|
||||
async verify(signature: string) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
177
packages/feature-auth/domain/src/usecases/LoginUseCase.ts
Normal file
177
packages/feature-auth/domain/src/usecases/LoginUseCase.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type { IAuthRepository, ITrustRepository, Credentials, UserSession, AuthConfig } from '../interfaces';
|
||||
import type { AuthResult } from '../entities';
|
||||
|
||||
/**
|
||||
* Login Use Case - Pure business logic
|
||||
* Orchestrates the login flow including risk assessment and DPoP initialization
|
||||
*
|
||||
* Features implemented (per FRD):
|
||||
* - F.ID.001: Login flows
|
||||
* - F.ID.004: DPoP integration
|
||||
* - Risk-adaptive authentication
|
||||
* - Step-up MFA based on risk score
|
||||
*/
|
||||
export class LoginUseCase {
|
||||
constructor(
|
||||
private authRepository: IAuthRepository,
|
||||
private trustRepository: ITrustRepository,
|
||||
private config: AuthConfig
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute login flow with risk assessment
|
||||
*/
|
||||
async execute(credentials: Credentials): Promise<AuthResult> {
|
||||
try {
|
||||
// 1. Risk Assessment (F.TR.004 from Trust SDK)
|
||||
const { score: riskScore, signals } = await this.trustRepository.calculateRiskScore();
|
||||
|
||||
// 2. Initialize DPoP (F.ID.004 / F.SC.004)
|
||||
const publicKey = await this.authRepository.initializeDPoP();
|
||||
|
||||
// 3. Attempt authentication
|
||||
const session = await this.authRepository.exchangeCredentialsForToken(credentials, publicKey);
|
||||
|
||||
// 4. Enhance session with risk information
|
||||
const enhancedSession: UserSession = {
|
||||
...session,
|
||||
riskScore,
|
||||
requiresStepUp: this.shouldRequireStepUp(riskScore, signals)
|
||||
};
|
||||
|
||||
// 5. Store session
|
||||
await this.authRepository.storeSession(enhancedSession);
|
||||
|
||||
// 6. Check if step-up authentication is required
|
||||
if (enhancedSession.requiresStepUp) {
|
||||
return {
|
||||
success: true,
|
||||
session: enhancedSession,
|
||||
requiresVerification: true,
|
||||
verificationMethod: this.determineStepUpMethod(signals),
|
||||
nextSteps: ['Complete additional verification to proceed']
|
||||
};
|
||||
}
|
||||
|
||||
// 7. Enable Remember Me if configured
|
||||
if (this.config.rememberMeDays > 0) {
|
||||
await this.authRepository.enableRememberMe(this.config.rememberMeDays);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
session: enhancedSession
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return this.handleLoginError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Biometric login flow
|
||||
*/
|
||||
async executeWithBiometrics(biometricSignature: string, deviceId: string): Promise<AuthResult> {
|
||||
const credentials: Credentials = {
|
||||
identifier: 'biometric',
|
||||
biometricSignature,
|
||||
deviceId
|
||||
};
|
||||
|
||||
return this.execute(credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic link login flow (F.ID.006)
|
||||
*/
|
||||
async executeWithMagicLink(token: string, nonce: string): Promise<AuthResult> {
|
||||
try {
|
||||
const session = await this.authRepository.verifyMagicLink(token, nonce);
|
||||
await this.authRepository.storeSession(session);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
session
|
||||
};
|
||||
} catch (error) {
|
||||
return this.handleLoginError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Federated login flow (F.ID.007)
|
||||
*/
|
||||
async executeWithProvider(provider: 'google' | 'apple', token: string): Promise<AuthResult> {
|
||||
try {
|
||||
const session = await this.authRepository.authenticateWithProvider(provider, token);
|
||||
await this.authRepository.storeSession(session);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
session
|
||||
};
|
||||
} catch (error) {
|
||||
return this.handleLoginError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRequireStepUp(riskScore: number, signals: Record<string, any>): boolean {
|
||||
// Risk-based step-up logic
|
||||
if (riskScore > 75) return true;
|
||||
if (signals.isRooted || signals.isEmulator) return true;
|
||||
if (signals.attestationFailed) return true;
|
||||
if (signals.geoDeviation && riskScore > 50) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private determineStepUpMethod(signals: Record<string, any>) {
|
||||
// Determine the most appropriate step-up method based on risk signals
|
||||
if (signals.suspiciousLocation) {
|
||||
return { type: 'otp' as const, target: 'phone' };
|
||||
}
|
||||
|
||||
if (signals.newDevice) {
|
||||
return { type: 'email' as const, target: 'email' };
|
||||
}
|
||||
|
||||
// Default to biometric if available
|
||||
return { type: 'biometric' as const };
|
||||
}
|
||||
|
||||
private handleLoginError(error: any): AuthResult {
|
||||
console.error('Login failed:', error);
|
||||
|
||||
// Map specific errors to user-friendly messages
|
||||
if (error.code === 'INVALID_CREDENTIALS') {
|
||||
return {
|
||||
success: false,
|
||||
errorCode: 'INVALID_CREDENTIALS',
|
||||
errorMessage: 'Invalid email or password'
|
||||
};
|
||||
}
|
||||
|
||||
if (error.code === 'ACCOUNT_LOCKED') {
|
||||
return {
|
||||
success: false,
|
||||
errorCode: 'ACCOUNT_LOCKED',
|
||||
errorMessage: 'Account is temporarily locked due to multiple failed attempts'
|
||||
};
|
||||
}
|
||||
|
||||
if (error.code === 'VERIFICATION_REQUIRED') {
|
||||
return {
|
||||
success: false,
|
||||
requiresVerification: true,
|
||||
verificationMethod: error.verificationMethod,
|
||||
errorMessage: 'Account verification required'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
errorCode: 'UNKNOWN_ERROR',
|
||||
errorMessage: 'An unexpected error occurred'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class RegistrationUseCase {
|
||||
async execute(payload: any) {
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class SessionUseCase {
|
||||
async getCurrent(sessionId: string) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
6
packages/feature-auth/ui-rn/package.json
Normal file
6
packages/feature-auth/ui-rn/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/auth/ui-rn",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-auth/ui-rn/src/index.ts
Normal file
1
packages/feature-auth/ui-rn/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const AuthUI = {};
|
||||
6
packages/feature-location/domain/package.json
Normal file
6
packages/feature-location/domain/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/location/domain",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-location/domain/src/index.ts
Normal file
1
packages/feature-location/domain/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const LocationDomain = {};
|
||||
6
packages/feature-location/ui-rn/package.json
Normal file
6
packages/feature-location/ui-rn/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/location/ui-rn",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-location/ui-rn/src/index.ts
Normal file
1
packages/feature-location/ui-rn/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const LocationUI = {};
|
||||
6
packages/feature-media/domain/package.json
Normal file
6
packages/feature-media/domain/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/media/domain",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-media/domain/src/index.ts
Normal file
1
packages/feature-media/domain/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MediaDomain = {};
|
||||
6
packages/feature-media/ui-rn/package.json
Normal file
6
packages/feature-media/ui-rn/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/media/ui-rn",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-media/ui-rn/src/index.ts
Normal file
1
packages/feature-media/ui-rn/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MediaUI = {};
|
||||
6
packages/feature-memberships/domain/package.json
Normal file
6
packages/feature-memberships/domain/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/memberships/domain",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-memberships/domain/src/index.ts
Normal file
1
packages/feature-memberships/domain/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MembershipsDomain = {};
|
||||
6
packages/feature-memberships/ui-rn/package.json
Normal file
6
packages/feature-memberships/ui-rn/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/memberships/ui-rn",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-memberships/ui-rn/src/index.ts
Normal file
1
packages/feature-memberships/ui-rn/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MembershipsUI = {};
|
||||
6
packages/feature-messaging/data/package.json
Normal file
6
packages/feature-messaging/data/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/messaging/data",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-messaging/data/src/index.ts
Normal file
1
packages/feature-messaging/data/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MessagingData = {};
|
||||
6
packages/feature-messaging/domain/package.json
Normal file
6
packages/feature-messaging/domain/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/messaging/domain",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-messaging/domain/src/index.ts
Normal file
1
packages/feature-messaging/domain/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MessagingDomain = {};
|
||||
6
packages/feature-messaging/ui-rn/package.json
Normal file
6
packages/feature-messaging/ui-rn/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/messaging/ui-rn",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-messaging/ui-rn/src/index.ts
Normal file
1
packages/feature-messaging/ui-rn/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MessagingUI = {};
|
||||
6
packages/feature-notes/domain/package.json
Normal file
6
packages/feature-notes/domain/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/notes/domain",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-notes/domain/src/index.ts
Normal file
1
packages/feature-notes/domain/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const NotesDomain = {};
|
||||
6
packages/feature-notes/ui-rn/package.json
Normal file
6
packages/feature-notes/ui-rn/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/notes/ui-rn",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-notes/ui-rn/src/index.ts
Normal file
1
packages/feature-notes/ui-rn/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const NotesUI = {};
|
||||
6
packages/feature-tasks/data/package.json
Normal file
6
packages/feature-tasks/data/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/tasks/data",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-tasks/data/src/index.ts
Normal file
1
packages/feature-tasks/data/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const TasksData = {};
|
||||
6
packages/feature-tasks/domain/package.json
Normal file
6
packages/feature-tasks/domain/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/tasks/domain",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-tasks/domain/src/index.ts
Normal file
1
packages/feature-tasks/domain/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const TasksDomain = {};
|
||||
6
packages/feature-tasks/ui-rn/package.json
Normal file
6
packages/feature-tasks/ui-rn/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@feature/tasks/ui-rn",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/feature-tasks/ui-rn/src/index.ts
Normal file
1
packages/feature-tasks/ui-rn/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const TasksUI = {};
|
||||
6
packages/shared/config/package.json
Normal file
6
packages/shared/config/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@shared/config",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/shared/config/src/index.ts
Normal file
1
packages/shared/config/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const sharedConfig = {};
|
||||
6
packages/shared/types/package.json
Normal file
6
packages/shared/types/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@shared/types",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/shared/types/src/index.ts
Normal file
1
packages/shared/types/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type AnyRecord = Record<string, any>;
|
||||
6
packages/shared/ui-kit/package.json
Normal file
6
packages/shared/ui-kit/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@shared/ui-kit",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts"
|
||||
}
|
||||
1
packages/shared/ui-kit/src/index.ts
Normal file
1
packages/shared/ui-kit/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const UIKIT = {};
|
||||
Reference in New Issue
Block a user