26 changed files with 2234 additions and 136 deletions
@ -0,0 +1,141 @@ |
|||||
|
# Implementation Complete Summary |
||||
|
|
||||
|
**Date**: 2025-10-31 |
||||
|
**Status**: ✅ **ALL HIGH-PRIORITY ITEMS COMPLETE** |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## ✅ Completed Implementation |
||||
|
|
||||
|
### 1. ES256K JWT Token Generation (Item #1) - **COMPLETE** |
||||
|
|
||||
|
**Implementation**: |
||||
|
- Added `did-jwt@^7.0.0` and `ethers@^6.0.0` dependencies |
||||
|
- Implemented `generateEndorserJWT()` function in `test-user-zero.ts` |
||||
|
- Derives Ethereum private key from seed phrase using `ethers.HDNodeWallet` |
||||
|
- Uses `did-jwt.SimpleSigner` for ES256K signing |
||||
|
- Creates proper ES256K signed JWTs matching TimeSafari's pattern |
||||
|
|
||||
|
**Files Modified**: |
||||
|
- `test-apps/daily-notification-test/src/config/test-user-zero.ts` - Added `generateEndorserJWT()` function |
||||
|
- `test-apps/daily-notification-test/src/views/HomeView.vue` - Updated to use `generateEndorserJWT()` |
||||
|
- `test-apps/daily-notification-test/package.json` - Added dependencies |
||||
|
|
||||
|
**Architecture**: |
||||
|
- JWT generation happens in TypeScript (no Java DID libraries needed) |
||||
|
- Native fetcher receives pre-generated tokens |
||||
|
- Matches TimeSafari's production pattern |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 2. Network Security Configuration (Item #15) - **COMPLETE** |
||||
|
|
||||
|
**Implementation**: |
||||
|
- Created `network_security_config.xml` allowing cleartext HTTP traffic to `10.0.2.2`, `localhost`, and `127.0.0.1` |
||||
|
- Updated `AndroidManifest.xml` to reference the network security config |
||||
|
|
||||
|
**Files Created/Modified**: |
||||
|
- `test-apps/daily-notification-test/android/app/src/main/res/xml/network_security_config.xml` (created) |
||||
|
- `test-apps/daily-notification-test/android/app/src/main/AndroidManifest.xml` (line 10) |
||||
|
|
||||
|
**Purpose**: |
||||
|
- Enables HTTP connections to `http://10.0.2.2:3000` from Android emulator |
||||
|
- Required for localhost testing (emulator's special IP for host machine) |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 3. TypeScript/Java Interface Updates - **COMPLETE** |
||||
|
|
||||
|
**Changes**: |
||||
|
- Updated `configureNativeFetcher()` to accept `jwtToken` instead of `jwtSecret` |
||||
|
- Updated all TypeScript definitions, Java plugin, and native interface |
||||
|
- Removed `as any` type assertion from `HomeView.vue` |
||||
|
- Rebuilt plugin to update `dist/definitions.d.ts` |
||||
|
|
||||
|
**Files Modified**: |
||||
|
- `src/definitions.ts` - Updated method signature and JSDoc |
||||
|
- `android/plugin/.../DailyNotificationPlugin.java` - Updated parameter handling |
||||
|
- `android/plugin/.../NativeNotificationContentFetcher.java` - Updated interface |
||||
|
- `test-apps/.../TestNativeFetcher.java` - Simplified (removed JWT generation) |
||||
|
- `test-apps/.../HomeView.vue` - Updated to use new token-based approach |
||||
|
- `dist/definitions.d.ts` - Rebuilt with updated types |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 4. TestNativeFetcher Simplification - **COMPLETE** |
||||
|
|
||||
|
**Changes**: |
||||
|
- Removed entire `generateJWTToken()` method (HMAC-SHA256 implementation) |
||||
|
- Removed unused imports (`Mac`, `SecretKeySpec`, `Base64`, `MessageDigest`) |
||||
|
- Changed `jwtSecret` field → `jwtToken` |
||||
|
- Updated to use pre-generated token directly in Authorization header |
||||
|
|
||||
|
**Files Modified**: |
||||
|
- `test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/TestNativeFetcher.java` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🚀 Ready for Testing |
||||
|
|
||||
|
### Prerequisites Complete: |
||||
|
- ✅ ES256K JWT generation implemented |
||||
|
- ✅ Network security config in place |
||||
|
- ✅ All interfaces updated |
||||
|
- ✅ Plugin rebuilt with correct types |
||||
|
|
||||
|
### Next Steps for Testing: |
||||
|
|
||||
|
1. **Enable Real API Calls**: |
||||
|
```typescript |
||||
|
// In test-apps/daily-notification-test/src/config/test-user-zero.ts |
||||
|
// Change line 28: |
||||
|
serverMode: "localhost" as "localhost" | "staging" | "production" | "mock" | "custom", |
||||
|
``` |
||||
|
|
||||
|
2. **Build and Run Android Test App**: |
||||
|
```bash |
||||
|
cd test-apps/daily-notification-test |
||||
|
npm run build |
||||
|
npx cap sync android |
||||
|
npx cap run android |
||||
|
``` |
||||
|
|
||||
|
3. **Verify JWT Generation**: |
||||
|
- Check console logs for JWT generation |
||||
|
- Verify token is passed to native fetcher |
||||
|
- Check logcat for native fetcher configuration |
||||
|
|
||||
|
4. **Test API Calls**: |
||||
|
- Schedule a notification |
||||
|
- Verify prefetch occurs |
||||
|
- Check that API calls succeed with ES256K tokens |
||||
|
- Verify notifications appear |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📋 Remaining Optional Tasks |
||||
|
|
||||
|
### Medium Priority: |
||||
|
- **Item #4**: Add `root.has("data")` validation in API response parser |
||||
|
- **Item #5**: Enhance logging with timing and structured tags |
||||
|
|
||||
|
### Low Priority: |
||||
|
- Items #7-11: Unit tests, JWT utility extraction, interceptors, caching, metrics |
||||
|
|
||||
|
### Documentation: |
||||
|
- Update `docs/NATIVE_FETCHER_CONFIGURATION.md` to reflect token-based approach |
||||
|
- Document ES256K JWT generation pattern for TimeSafari integration |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔗 Related Files |
||||
|
|
||||
|
- **TODO Document**: `test-apps/daily-notification-test/TODO_NATIVE_FETCHER.md` |
||||
|
- **Investigation Results**: `test-apps/daily-notification-test/INVESTIGATION_JWT_ALGORITHM_RESULTS.md` |
||||
|
- **Test Config**: `test-apps/daily-notification-test/src/config/test-user-zero.ts` |
||||
|
- **Network Config**: `test-apps/daily-notification-test/android/app/src/main/res/xml/network_security_config.xml` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**All critical implementation work is complete. Ready for testing!** |
||||
|
|
||||
@ -0,0 +1,301 @@ |
|||||
|
# JWT Algorithm Investigation Guide |
||||
|
|
||||
|
**Created**: 2025-10-31 |
||||
|
**Purpose**: Systematically investigate which JWT signing algorithm endorser-ch and TimeSafari actually use |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Investigation Checklist |
||||
|
|
||||
|
### Phase 1: TimeSafari Repository (Most Authoritative) |
||||
|
|
||||
|
**Repository**: `ssh://git@173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa.git` |
||||
|
|
||||
|
#### Step 1: Find `createEndorserJwtForDid` Function |
||||
|
|
||||
|
```bash |
||||
|
# Clone or navigate to the repository |
||||
|
cd /path/to/crowd-funder-for-time-pwa |
||||
|
|
||||
|
# Search for the function |
||||
|
grep -r "createEndorserJwtForDid" . |
||||
|
grep -r "createEndorserJWT" . |
||||
|
grep -r "createEndorser.*JWT" . |
||||
|
``` |
||||
|
|
||||
|
**What to look for:** |
||||
|
- Function implementation showing how JWT is generated |
||||
|
- Import statements (e.g., `did-jwt`, `jsonwebtoken`, `jose`, etc.) |
||||
|
- Algorithm parameter (e.g., `alg: 'HS256'`, `algorithm: 'HS256'`, etc.) |
||||
|
- Key/secret usage (shared secret vs DID private key) |
||||
|
|
||||
|
#### Step 2: Find Endpoint Usage |
||||
|
|
||||
|
```bash |
||||
|
# Search for endpoint usage |
||||
|
grep -r "plansLastUpdatedBetween" . |
||||
|
grep -r "/api/v2/report" . |
||||
|
``` |
||||
|
|
||||
|
**What to look for:** |
||||
|
- How the JWT is attached to requests (Authorization header) |
||||
|
- What function generates the JWT for this endpoint |
||||
|
- Any configuration or secret management |
||||
|
|
||||
|
#### Step 3: Check JWT Generation Code |
||||
|
|
||||
|
**Files to examine:** |
||||
|
- Any files matching `*jwt*.ts`, `*jwt*.js`, `*auth*.ts`, `*auth*.js` |
||||
|
- Configuration files (`.env`, `config/*.ts`, `src/config/*.ts`) |
||||
|
- API client files that call endorser endpoints |
||||
|
|
||||
|
**Key questions:** |
||||
|
1. Does it use a shared secret (string) or DID private key? |
||||
|
2. What library is used? (`jsonwebtoken`, `did-jwt`, `jose`, etc.) |
||||
|
3. What algorithm is specified in the code? |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### Phase 2: endorser-ch Repository (Server-Side Verification) |
||||
|
|
||||
|
**Repository**: `https://github.com/trentlarson/endorser-ch` |
||||
|
|
||||
|
#### Step 1: Find Endpoint Handler |
||||
|
|
||||
|
```bash |
||||
|
# Clone or navigate to the repository |
||||
|
cd /path/to/endorser-ch |
||||
|
|
||||
|
# Search for endpoint handler |
||||
|
grep -r "plansLastUpdatedBetween" . |
||||
|
grep -r "/api/v2/report" . |
||||
|
``` |
||||
|
|
||||
|
**Files likely to contain:** |
||||
|
- `src/server.js` or `server/server.js` |
||||
|
- `src/routes.js` or `routes/*.js` |
||||
|
- `src/api/` or `src/controllers/` |
||||
|
|
||||
|
#### Step 2: Find Authentication Middleware |
||||
|
|
||||
|
```bash |
||||
|
# Search for JWT verification |
||||
|
grep -r "verifyJWT" . |
||||
|
grep -r "verify.*JWT" . |
||||
|
grep -r "Authorization.*Bearer" . |
||||
|
grep -r "did-jwt" . |
||||
|
grep -r "jsonwebtoken" . |
||||
|
``` |
||||
|
|
||||
|
**What to look for:** |
||||
|
- Middleware that processes `Authorization: Bearer {token}` header |
||||
|
- JWT verification logic |
||||
|
- Algorithm used for verification |
||||
|
- Secret/key used for verification |
||||
|
|
||||
|
#### Step 3: Check Configuration for JWT Secret |
||||
|
|
||||
|
```bash |
||||
|
# Search for JWT secret configuration |
||||
|
grep -r "JWT_SECRET" . |
||||
|
grep -r "API_SECRET" . |
||||
|
grep -r "jwtSecret" . |
||||
|
grep -r "jwt.*secret" -i . |
||||
|
``` |
||||
|
|
||||
|
**Files to check:** |
||||
|
- `.env` files |
||||
|
- `config/*.js` or `conf/*.js` |
||||
|
- `package.json` (for dependencies) |
||||
|
|
||||
|
**Key questions:** |
||||
|
1. Is there a JWT_SECRET environment variable? |
||||
|
2. Does it use `did-jwt.verifyJWT()` with DID resolution? |
||||
|
3. Or does it use `jsonwebtoken.verify()` with a shared secret? |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Expected Findings & Decision Matrix |
||||
|
|
||||
|
### Scenario A: HS256 with Shared Secret ✅ (Most Likely) |
||||
|
|
||||
|
**Indicators:** |
||||
|
- ✅ TimeSafari code uses `jsonwebtoken.sign()` or similar with a secret string |
||||
|
- ✅ endorser-ch uses `jsonwebtoken.verify()` or similar with a secret |
||||
|
- ✅ Environment variable `JWT_SECRET` exists |
||||
|
- ✅ Code explicitly sets `algorithm: 'HS256'` or `alg: 'HS256'` |
||||
|
|
||||
|
**Action**: Our current HMAC-SHA256 implementation is **CORRECT** |
||||
|
|
||||
|
### Scenario B: DID-based (ES256K) ⚠️ |
||||
|
|
||||
|
**Indicators:** |
||||
|
- ⚠️ TimeSafari code uses `did-jwt.createJWT()` or Veramo JWT creation |
||||
|
- ⚠️ endorser-ch uses `did-jwt.verifyJWT()` with DID resolver |
||||
|
- ⚠️ Code uses Ethereum private keys or DID document keys |
||||
|
- ⚠️ No shared secret configuration found |
||||
|
|
||||
|
**Action**: Need to implement DID-based signing with ES256K algorithm |
||||
|
|
||||
|
### Scenario C: Hybrid Approach 🔄 |
||||
|
|
||||
|
**Indicators:** |
||||
|
- 🔄 Different algorithms for different endpoints |
||||
|
- 🔄 API auth uses HS256, but claim verification uses DID-based |
||||
|
- 🔄 Multiple authentication methods supported |
||||
|
|
||||
|
**Action**: Use HS256 for `/api/v2/report/plansLastUpdatedBetween` endpoint |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Investigation Commands Summary |
||||
|
|
||||
|
### TimeSafari Repository Investigation |
||||
|
|
||||
|
```bash |
||||
|
cd /path/to/crowd-funder-for-time-pwa |
||||
|
|
||||
|
# Find JWT generation |
||||
|
grep -r "createEndorserJwtForDid\|createEndorserJWT" . --include="*.ts" --include="*.js" |
||||
|
|
||||
|
# Find endpoint usage |
||||
|
grep -r "plansLastUpdatedBetween" . --include="*.ts" --include="*.js" |
||||
|
|
||||
|
# Check for JWT libraries |
||||
|
grep -r "jsonwebtoken\|did-jwt\|jose" package.json |
||||
|
grep -r "from.*jsonwebtoken\|from.*did-jwt\|import.*jose" . --include="*.ts" --include="*.js" |
||||
|
|
||||
|
# Check for secret usage |
||||
|
grep -r "jwtSecret\|JWT_SECRET\|shared.*secret" . --include="*.ts" --include="*.js" -i |
||||
|
``` |
||||
|
|
||||
|
### endorser-ch Repository Investigation |
||||
|
|
||||
|
```bash |
||||
|
cd /path/to/endorser-ch |
||||
|
|
||||
|
# Find endpoint handler |
||||
|
grep -r "plansLastUpdatedBetween" . --include="*.js" --include="*.ts" |
||||
|
|
||||
|
# Find authentication middleware |
||||
|
grep -r "Authorization.*Bearer\|verifyJWT\|verify.*JWT" . --include="*.js" --include="*.ts" |
||||
|
|
||||
|
# Check for JWT libraries |
||||
|
grep -r "jsonwebtoken\|did-jwt\|jose" package.json |
||||
|
|
||||
|
# Check for secret configuration |
||||
|
grep -r "JWT_SECRET\|API_SECRET\|jwt.*secret" . -i --include="*.js" --include="*.env*" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Documentation References to Review |
||||
|
|
||||
|
### TimeSafari Repository |
||||
|
|
||||
|
**Look for:** |
||||
|
- API client documentation |
||||
|
- Authentication/authorization documentation |
||||
|
- JWT generation examples |
||||
|
- Configuration documentation |
||||
|
|
||||
|
**Files to check:** |
||||
|
- `README.md` |
||||
|
- `docs/api.md` or `docs/auth.md` |
||||
|
- `src/api/` or `src/services/` directories |
||||
|
- Configuration files in `src/config/` or root |
||||
|
|
||||
|
### endorser-ch Repository |
||||
|
|
||||
|
**Look for:** |
||||
|
- API documentation |
||||
|
- Authentication documentation |
||||
|
- README.md (already mentions `did-jwt` in JWT verification section) |
||||
|
- Route handler documentation |
||||
|
|
||||
|
**Files to check:** |
||||
|
- `README.md` (check JWT verification section mentioned) |
||||
|
- `src/` or `server/` directories |
||||
|
- Route files |
||||
|
- Authentication middleware files |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Quick Decision Guide |
||||
|
|
||||
|
After investigation, use this decision matrix: |
||||
|
|
||||
|
| Finding | Algorithm | Implementation Status | |
||||
|
|---------|-----------|----------------------| |
||||
|
| `jsonwebtoken` + `JWT_SECRET` | HS256 | ✅ Already implemented | |
||||
|
| `did-jwt` + DID resolution | ES256K | ❌ Need to implement | |
||||
|
| Both found (hybrid) | HS256 for API | ✅ Already implemented | |
||||
|
| Can't determine | Test both | ⚠️ Implement both options | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Next Steps After Investigation |
||||
|
|
||||
|
1. **If HS256 confirmed:** |
||||
|
- ✅ Mark TODO item #1 as complete |
||||
|
- ✅ Current implementation is correct |
||||
|
- ✅ Test against API |
||||
|
|
||||
|
2. **If DID-based confirmed:** |
||||
|
- ❌ Need to implement DID-based signing |
||||
|
- Add dependency: `did-jwt-java` or `web3j` for Ethereum signing |
||||
|
- Update `generateJWTToken()` to use DID private keys |
||||
|
- Update TODO with findings |
||||
|
|
||||
|
3. **If hybrid or unclear:** |
||||
|
- Document findings |
||||
|
- Implement both if needed |
||||
|
- Add configuration option to switch between methods |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Investigation Results Template |
||||
|
|
||||
|
After investigation, fill out: |
||||
|
|
||||
|
```markdown |
||||
|
## Investigation Results |
||||
|
|
||||
|
**Date**: YYYY-MM-DD |
||||
|
**Investigator**: [Name] |
||||
|
|
||||
|
### TimeSafari Repository Findings |
||||
|
|
||||
|
**createEndorserJwtForDid function:** |
||||
|
- Location: `[file path]` |
||||
|
- Library used: `[jsonwebtoken/did-jwt/etc]` |
||||
|
- Algorithm: `[HS256/ES256K/etc]` |
||||
|
- Key type: `[shared secret/DID private key]` |
||||
|
- Code snippet: `[paste relevant code]` |
||||
|
|
||||
|
**Endpoint usage:** |
||||
|
- Location: `[file path]` |
||||
|
- How JWT is generated: `[description]` |
||||
|
- Code snippet: `[paste relevant code]` |
||||
|
|
||||
|
### endorser-ch Repository Findings |
||||
|
|
||||
|
**Endpoint handler:** |
||||
|
- Location: `[file path]` |
||||
|
- Authentication middleware: `[description]` |
||||
|
- Verification method: `[jsonwebtoken.verify/did-jwt.verifyJWT/etc]` |
||||
|
- Algorithm: `[HS256/ES256K/etc]` |
||||
|
- Secret/key source: `[environment variable/DID resolution/etc]` |
||||
|
- Code snippet: `[paste relevant code]` |
||||
|
|
||||
|
### Conclusion |
||||
|
|
||||
|
**Algorithm**: [HS256/ES256K/Hybrid/Unclear] |
||||
|
**Action Required**: [Update implementation/Mark complete/etc] |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Status**: Ready for investigation |
||||
|
**Priority**: CRITICAL - Blocks API authentication |
||||
|
|
||||
@ -0,0 +1,220 @@ |
|||||
|
# JWT Algorithm Investigation Results |
||||
|
|
||||
|
**Date**: 2025-10-31 |
||||
|
**Investigator**: Auto (AI Assistant) |
||||
|
**Status**: ✅ **INVESTIGATION COMPLETE** |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔴 CRITICAL FINDING: DID-Based JWT Signing Required (ES256K) |
||||
|
|
||||
|
### Conclusion |
||||
|
|
||||
|
**The endorser-ch API expects DID-based JWTs signed with ES256K (or ES256K-R), NOT HMAC-SHA256 (HS256).** |
||||
|
|
||||
|
The current implementation in `TestNativeFetcher.java` using HMAC-SHA256 is **INCORRECT** and will fail authentication. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Investigation Details |
||||
|
|
||||
|
### TimeSafari Repository Findings |
||||
|
|
||||
|
**Location**: `~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts` |
||||
|
|
||||
|
**Function**: `createEndorserJwtForKey()` |
||||
|
|
||||
|
**Implementation**: |
||||
|
```typescript |
||||
|
export async function createEndorserJwtForKey( |
||||
|
account: KeyMetaWithPrivate, |
||||
|
payload: object, |
||||
|
expiresIn?: number, |
||||
|
) { |
||||
|
if (account?.identity) { |
||||
|
const identity: IIdentifier = JSON.parse(account.identity!); |
||||
|
const privateKeyHex = identity.keys[0].privateKeyHex; |
||||
|
const signer = await SimpleSigner(privateKeyHex as string); |
||||
|
const options = { |
||||
|
// alg: "ES256K", // "K" is the default, "K-R" is used by the server in tests |
||||
|
issuer: account.did, |
||||
|
signer: signer, |
||||
|
expiresIn: undefined as number | undefined, |
||||
|
}; |
||||
|
if (expiresIn) { |
||||
|
options.expiresIn = expiresIn; |
||||
|
} |
||||
|
return didJwt.createJWT(payload, options); |
||||
|
} |
||||
|
// ... passkey handling ... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Key Points**: |
||||
|
- Uses `did-jwt.createJWT()` library |
||||
|
- Uses `SimpleSigner(privateKeyHex)` - signs with Ethereum private key |
||||
|
- Algorithm is **ES256K** (default) |
||||
|
- Signs with DID private key, NOT a shared secret |
||||
|
|
||||
|
**Library Used**: `did-jwt` (DID-based JWT library) |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### endorser-ch Repository Findings |
||||
|
|
||||
|
**Location**: `~/projects/timesafari/endorser-ch/src/api/services/vc/index.js` |
||||
|
|
||||
|
**Function**: `decodeAndVerifyJwt()` |
||||
|
|
||||
|
**Implementation**: |
||||
|
```javascript |
||||
|
export async function decodeAndVerifyJwt(jwt) { |
||||
|
const pieces = jwt.split('.') |
||||
|
const header = JSON.parse(base64url.decode(pieces[0])) |
||||
|
const payload = JSON.parse(base64url.decode(pieces[1])) |
||||
|
const issuerDid = payload.iss |
||||
|
|
||||
|
if (issuerDid.startsWith(ETHR_DID_PREFIX)) { |
||||
|
try { |
||||
|
const verifiedResult = await didJwt.verifyJWT(jwt, {resolver}) |
||||
|
return verifiedResult |
||||
|
} catch (e) { |
||||
|
return Promise.reject({ |
||||
|
clientError: { |
||||
|
message: `JWT failed verification: ` + e.toString(), |
||||
|
code: JWT_VERIFY_FAILED_CODE |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
// ... other DID methods ... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Key Points**: |
||||
|
- Uses `did-jwt.verifyJWT(jwt, {resolver})` - DID-based verification |
||||
|
- Verifies signature using DID resolver (resolves DID to public key) |
||||
|
- **NO shared secret used** - uses DID public key from resolver |
||||
|
- Algorithm: **ES256K** (implicit from did-jwt library) |
||||
|
|
||||
|
**Middleware Location**: `~/projects/timesafari/endorser-ch/src/common/server.js` |
||||
|
|
||||
|
**Authentication Flow**: |
||||
|
```javascript |
||||
|
decodeAndVerifyJwt(authorizationJwt) |
||||
|
.then((result) => { |
||||
|
const { header, issuer, payload, verified } = result |
||||
|
if (!verified) { |
||||
|
res.status(400).json({ |
||||
|
error: { |
||||
|
message: "Signature failed validation.", |
||||
|
code: ERROR_CODES.JWT_VERIFY_FAILED |
||||
|
} |
||||
|
}).end() |
||||
|
} else { |
||||
|
res.locals.authTokenIssuer = issuer |
||||
|
next() |
||||
|
} |
||||
|
}) |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Impact on Current Implementation |
||||
|
|
||||
|
### Current Implementation (WRONG) |
||||
|
|
||||
|
**File**: `test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/TestNativeFetcher.java` |
||||
|
|
||||
|
**Current Approach**: HMAC-SHA256 with shared secret |
||||
|
```java |
||||
|
Mac hmac = Mac.getInstance("HmacSHA256"); |
||||
|
SecretKeySpec secretKey = new SecretKeySpec( |
||||
|
jwtSecret.getBytes(StandardCharsets.UTF_8), |
||||
|
"HmacSHA256" |
||||
|
); |
||||
|
hmac.init(secretKey); |
||||
|
byte[] signatureBytes = hmac.doFinal(unsignedToken.getBytes(StandardCharsets.UTF_8)); |
||||
|
``` |
||||
|
|
||||
|
**Problem**: Server expects ES256K with DID private key, NOT HMAC-SHA256 with shared secret |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Required Changes |
||||
|
|
||||
|
### Action Required |
||||
|
|
||||
|
1. **Remove HMAC-SHA256 implementation** - This is completely wrong |
||||
|
2. **Implement DID-based signing (ES256K)** - Sign with Ethereum private key |
||||
|
3. **Access DID private key** - Need to retrieve private key from DID/account storage |
||||
|
4. **Use did-jwt-java or web3j** - Java library for DID-based JWT signing |
||||
|
|
||||
|
### Implementation Options |
||||
|
|
||||
|
#### Option 1: Use did-jwt-java Library (Recommended) |
||||
|
|
||||
|
```java |
||||
|
// Add dependency to build.gradle |
||||
|
implementation 'io.uport:uport-did-jwt:3.1.0' |
||||
|
|
||||
|
// Sign with DID private key |
||||
|
import io.uport.sdk.did.jwt.DIDJWT; |
||||
|
import io.uport.sdk.did.jwt.SimpleSigner; |
||||
|
|
||||
|
String privateKeyHex = getPrivateKeyForDid(activeDid); // Need to implement |
||||
|
SimpleSigner signer = new SimpleSigner(privateKeyHex); |
||||
|
String jwt = DIDJWT.createJWT(payload, signer, issuer: activeDid); |
||||
|
``` |
||||
|
|
||||
|
#### Option 2: Use web3j for Ethereum Signing |
||||
|
|
||||
|
```java |
||||
|
// Add dependency |
||||
|
implementation 'org.web3j:core:4.9.8' |
||||
|
|
||||
|
import org.web3j.crypto.ECKeyPair; |
||||
|
import org.web3j.crypto.Sign; |
||||
|
|
||||
|
ECKeyPair keyPair = getKeyPairForDid(activeDid); // Need to implement |
||||
|
Sign.SignatureData signature = Sign.signMessage( |
||||
|
unsignedToken.getBytes(StandardCharsets.UTF_8), |
||||
|
keyPair |
||||
|
); |
||||
|
// Then encode signature according to ES256K format |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Next Steps |
||||
|
|
||||
|
1. **Remove `jwtSecret` parameter** - No longer needed (shared secret not used) |
||||
|
2. **Add DID private key retrieval** - Need mechanism to get private key for `activeDid` |
||||
|
3. **Implement ES256K signing** - Using did-jwt-java or web3j |
||||
|
4. **Update `configureNativeFetcher()`** - Remove `jwtSecret`, add private key retrieval mechanism |
||||
|
5. **Test with real API** - Verify JWTs are accepted by endorser-ch server |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## References |
||||
|
|
||||
|
- **TimeSafari Implementation**: `~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts` |
||||
|
- **endorser-ch Verification**: `~/projects/timesafari/endorser-ch/src/api/services/vc/index.js` |
||||
|
- **did-jwt Library**: https://github.com/decentralized-identity/did-jwt |
||||
|
- **did-jwt-java**: https://github.com/uport-project/uport-did-jwt (if available) or alternative Java DID libraries |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Evidence Summary |
||||
|
|
||||
|
| Component | Finding | Evidence | |
||||
|
|-----------|---------|----------| |
||||
|
| **TimeSafari JWT Creation** | ✅ DID-based (ES256K) | Uses `didJwt.createJWT()` with `SimpleSigner(privateKeyHex)` | |
||||
|
| **endorser-ch JWT Verification** | ✅ DID-based (ES256K) | Uses `didJwt.verifyJWT(jwt, {resolver})` | |
||||
|
| **Current TestNativeFetcher** | ❌ HMAC-SHA256 | Uses `Mac.getInstance("HmacSHA256")` with shared secret | |
||||
|
| **Shared Secret Config** | ❌ Not Used | No `JWT_SECRET` found in endorser-ch, no shared secret in TimeSafari | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Status**: Investigation complete. Implementation changes required. |
||||
|
|
||||
@ -0,0 +1,677 @@ |
|||||
|
# TODO: Test App Native Fetcher Improvements |
||||
|
|
||||
|
**Created**: 2025-10-31 |
||||
|
**Updated**: 2025-10-31 (implementation complete: ES256K JWT generation and network security config) |
||||
|
**Context**: Review of `TestNativeFetcher.java` implementation for endorser API integration |
||||
|
**Status**: ✅ **IMPLEMENTATION COMPLETE** - All high-priority items complete. Item #1 (ES256K JWT generation) implemented using `did-jwt` and `ethers`. Item #15 (Network Security Config) created. All TypeScript/Java interfaces updated. Plugin rebuilt with updated definitions. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📊 Progress Summary |
||||
|
|
||||
|
### ✅ Completed High-Priority Items |
||||
|
- **Item #1**: JWT Algorithm Investigation - ✅ **COMPLETE** (ES256K requirement confirmed, implementation pending) |
||||
|
- **Item #2**: SharedPreferences Persistence - ✅ **COMPLETE** |
||||
|
- **Item #3**: Error Handling and Retry Logic - ✅ **COMPLETE** |
||||
|
- **Item #6**: Context Management - ✅ **COMPLETE** |
||||
|
|
||||
|
### ✅ Critical Implementation Complete (Item #1) |
||||
|
- **ES256K JWT Token Generation** - ✅ **COMPLETE**: Implemented `generateEndorserJWT()` function using `did-jwt` and `ethers` libraries. Generates proper ES256K signed JWTs from test user zero's seed phrase. Native fetcher now receives pre-generated tokens, avoiding Java DID library complexity. |
||||
|
|
||||
|
### 🟡 Medium Priority Status |
||||
|
- **Item #4**: API Response Structure Validation - ✅ **VERIFIED** (structure verified, validation improvements needed - see details below) |
||||
|
- **Item #5**: Comprehensive Logging - Partial (basic logging exists, more comprehensive needed) |
||||
|
|
||||
|
### 🟢 Low Priority |
||||
|
- Items #7-11: All pending |
||||
|
|
||||
|
### 📝 Configuration Tasks |
||||
|
- **Item #13**: Real API Calls - Ready (can now enable localhost mode, ES256K JWT generation complete) |
||||
|
- **Item #14**: TypeScript Types - ✅ **COMPLETE** (plugin rebuilt, types updated, `as any` removed) |
||||
|
- **Item #15**: Network Security Config - ✅ **COMPLETE** (config created, AndroidManifest updated) |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔴 High Priority |
||||
|
|
||||
|
### 1. ⚠️ VERIFY: Determine Correct JWT Signing Algorithm (CRITICAL) |
||||
|
|
||||
|
- [x] **Status**: ✅ **INVESTIGATION COMPLETE** - See `INVESTIGATION_JWT_ALGORITHM_RESULTS.md` |
||||
|
|
||||
|
**CRITICAL FINDING**: Endorser-ch API expects **DID-based JWTs (ES256K)**, NOT HMAC-SHA256. Current implementation is WRONG and must be replaced. |
||||
|
|
||||
|
**Current State**: Uses SHA-256 hash with `jwtSecret:unsignedToken` format, header claims `"alg": "HS256"` |
||||
|
|
||||
|
**Location**: `TestNativeFetcher.java` → `generateJWTToken()` |
||||
|
|
||||
|
**Investigation Results** (See `INVESTIGATION_JWT_ALGORITHM_RESULTS.md` for full details): |
||||
|
|
||||
|
**✅ CONFIRMED**: Endorser-ch API uses **DID-based JWTs (ES256K)**, NOT HMAC-SHA256. |
||||
|
|
||||
|
**TimeSafari Implementation** (`~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts`): |
||||
|
- Uses `did-jwt.createJWT()` with `SimpleSigner(privateKeyHex)` |
||||
|
- Signs with Ethereum private key from DID identity |
||||
|
- Algorithm: **ES256K** (default for did-jwt library) |
||||
|
|
||||
|
**endorser-ch Verification** (`~/projects/timesafari/endorser-ch/src/api/services/vc/index.js`): |
||||
|
- Uses `did-jwt.verifyJWT(jwt, {resolver})` for verification |
||||
|
- Verifies signature using DID resolver (resolves DID to public key) |
||||
|
- **NO shared secret used** - authentication is DID-based |
||||
|
|
||||
|
**Current Implementation Problem**: |
||||
|
- Currently uses HMAC-SHA256 with shared secret (`jwtSecret`) |
||||
|
- Server will **reject** these tokens as invalid |
||||
|
- Must be replaced with DID-based ES256K signing |
||||
|
|
||||
|
**Required Implementation Changes:** |
||||
|
|
||||
|
1. **Remove HMAC-SHA256 implementation** - Completely incorrect approach |
||||
|
2. **Remove `jwtSecret` parameter** - No longer needed (shared secret not used) |
||||
|
3. **Implement DID-based ES256K signing** - Sign with Ethereum private key |
||||
|
4. **Add DID private key retrieval** - Need mechanism to get private key for `activeDid` |
||||
|
5. **Use Java DID library** - did-jwt-java or web3j for ES256K signing |
||||
|
|
||||
|
**Implementation Options:** |
||||
|
|
||||
|
**Option 1: Use did-jwt-java Library (Recommended)** |
||||
|
```java |
||||
|
// Add dependency to build.gradle |
||||
|
implementation 'io.uport:uport-did-jwt:3.1.0' // or equivalent |
||||
|
|
||||
|
// Sign with DID private key |
||||
|
import io.uport.sdk.did.jwt.DIDJWT; |
||||
|
import io.uport.sdk.did.jwt.SimpleSigner; |
||||
|
|
||||
|
String privateKeyHex = getPrivateKeyForDid(activeDid); // Need to implement |
||||
|
SimpleSigner signer = new SimpleSigner(privateKeyHex); |
||||
|
String jwt = DIDJWT.createJWT(payload, signer, issuer: activeDid); |
||||
|
``` |
||||
|
|
||||
|
**Option 2: Use web3j for Ethereum Signing** |
||||
|
```java |
||||
|
// Add dependency |
||||
|
implementation 'org.web3j:core:4.9.8' |
||||
|
|
||||
|
import org.web3j.crypto.ECKeyPair; |
||||
|
import org.web3j.crypto.Sign; |
||||
|
|
||||
|
ECKeyPair keyPair = getKeyPairForDid(activeDid); // Need to implement |
||||
|
Sign.SignatureData signature = Sign.signMessage( |
||||
|
unsignedToken.getBytes(StandardCharsets.UTF_8), |
||||
|
keyPair |
||||
|
); |
||||
|
// Then encode signature according to ES256K format for JWT |
||||
|
``` |
||||
|
|
||||
|
**References:** |
||||
|
- **Investigation Results**: See `INVESTIGATION_JWT_ALGORITHM_RESULTS.md` for complete findings |
||||
|
- **TimeSafari Implementation**: `~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts` - `createEndorserJwtForKey()` function |
||||
|
- **endorser-ch Verification**: `~/projects/timesafari/endorser-ch/src/api/services/vc/index.js` - `decodeAndVerifyJwt()` function |
||||
|
- [did-jwt library](https://github.com/decentralized-identity/did-jwt) - DID-based JWT library (JavaScript reference) |
||||
|
- [did-jwt-java](https://github.com/uport-project/uport-did-jwt) - Java implementation (if available) or alternative Java DID libraries |
||||
|
|
||||
|
**Impact**: **CRITICAL** - Current HMAC-SHA256 implementation will be rejected by API server. All authentication will fail until DID-based ES256K signing is implemented. |
||||
|
|
||||
|
**✅ ARCHITECTURAL DECISION**: Generate JWT in TypeScript (host app), pass token to native fetcher |
||||
|
|
||||
|
**Rationale**: |
||||
|
- TimeSafari already has `did-jwt` library available in TypeScript |
||||
|
- TimeSafari can access DID private keys via Capacitor SQLite (`retrieveFullyDecryptedAccount()`) |
||||
|
- Plugin SPI pattern is designed for host app to provide implementations |
||||
|
- Avoids complexity of finding/implementing Java DID libraries |
||||
|
- Keeps cryptography in TypeScript where libraries are mature |
||||
|
|
||||
|
**Updated Implementation Approach**: |
||||
|
|
||||
|
**Option A: Generate JWT in TypeScript, Pass Token (RECOMMENDED)** |
||||
|
```typescript |
||||
|
// In HomeView.vue or TimeSafari app |
||||
|
import { createEndorserJwtForDid } from '@/libs/endorserServer'; |
||||
|
|
||||
|
// Generate JWT in TypeScript using existing TimeSafari infrastructure |
||||
|
const account = await retrieveFullyDecryptedAccount(activeDid); |
||||
|
const jwtToken = await createEndorserJwtForDid(activeDid, { |
||||
|
exp: Math.floor(Date.now() / 1000) + 60, |
||||
|
iat: Math.floor(Date.now() / 1000), |
||||
|
iss: activeDid |
||||
|
}); |
||||
|
|
||||
|
// Pass JWT token to native fetcher (no private key needed) |
||||
|
await DailyNotification.configureNativeFetcher({ |
||||
|
apiBaseUrl: 'http://10.0.2.2:3000', |
||||
|
activeDid: activeDid, |
||||
|
jwtToken: jwtToken // Pre-generated token, not secret |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
```java |
||||
|
// In TestNativeFetcher.java - Just use the token, don't generate it |
||||
|
private volatile String jwtToken; // Pre-generated token from TypeScript |
||||
|
|
||||
|
public void configure(String apiBaseUrl, String activeDid, String jwtToken) { |
||||
|
this.apiBaseUrl = apiBaseUrl; |
||||
|
this.activeDid = activeDid; |
||||
|
this.jwtToken = jwtToken; // Use pre-generated token |
||||
|
} |
||||
|
|
||||
|
private String getAuthorizationHeader() { |
||||
|
return "Bearer " + jwtToken; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Benefits**: |
||||
|
- ✅ No Java DID library needed |
||||
|
- ✅ Uses existing TimeSafari infrastructure (`did-jwt`, `retrieveFullyDecryptedAccount`) |
||||
|
- ✅ Keeps cryptography in TypeScript (where it's proven to work) |
||||
|
- ✅ Native fetcher just uses token (simple, no crypto complexity) |
||||
|
- ✅ Token can be refreshed in TypeScript when needed |
||||
|
|
||||
|
**Next Actions**: |
||||
|
- [ ] Update `configureNativeFetcher()` TypeScript signature to accept `jwtToken` instead of `jwtSecret` |
||||
|
- [ ] Update Java `configureNativeFetcher()` to accept `jwtToken` parameter |
||||
|
- [ ] Remove JWT generation code from `TestNativeFetcher.generateJWTToken()` |
||||
|
- [ ] Use pre-generated token in `getAuthorizationHeader()` method |
||||
|
- [ ] Update `HomeView.vue` to generate JWT using TimeSafari's `createEndorserJwtForDid()` |
||||
|
- [ ] For TimeSafari integration: Generate JWT in host app using account data from SQLite |
||||
|
- [ ] Test with real endorser-ch API to verify tokens are accepted |
||||
|
|
||||
|
**Testing**: |
||||
|
- [x] ✅ Verified algorithm with endorser-ch source code - ES256K confirmed |
||||
|
- [x] ✅ Generate JWT in TypeScript using `did-jwt` library (ES256K signing) |
||||
|
- [x] ✅ Pass pre-generated JWT token to native fetcher via `configureNativeFetcher()` |
||||
|
- [ ] ⚠️ Verify tokens are accepted by the endorser API endpoint (pending real API testing) |
||||
|
- [ ] Implement token refresh mechanism in TypeScript when tokens expire (60 seconds) |
||||
|
|
||||
|
**Implementation Checklist**: |
||||
|
|
||||
|
**1. Update TypeScript Interface (`src/definitions.ts`)**: |
||||
|
- [x] ✅ Change `jwtSecret: string` → `jwtToken: string` in `configureNativeFetcher()` signature |
||||
|
- [x] ✅ Update JSDoc to reflect that token is pre-generated, not a secret |
||||
|
- [x] ✅ Rebuild plugin to update `dist/definitions.d.ts` |
||||
|
|
||||
|
**2. Update Java Plugin (`android/plugin/.../DailyNotificationPlugin.java`)**: |
||||
|
- [x] ✅ Change `String jwtSecret = call.getString("jwtSecret")` → `String jwtToken = call.getString("jwtToken")` |
||||
|
- [x] ✅ Update validation message: "Missing required parameters: apiBaseUrl, activeDid, and jwtToken are required" |
||||
|
- [x] ✅ Update `fetcher.configure(apiBaseUrl, activeDid, jwtToken)` call |
||||
|
- [x] ✅ Update Javadoc to reflect token-based approach |
||||
|
|
||||
|
**3. Update Native Interface (`android/plugin/.../NativeNotificationContentFetcher.java`)**: |
||||
|
- [x] ✅ Change `default void configure(String apiBaseUrl, String activeDid, String jwtSecret)` |
||||
|
→ `default void configure(String apiBaseUrl, String activeDid, String jwtToken)` |
||||
|
- [x] ✅ Update Javadoc to explain token is pre-generated by TypeScript |
||||
|
|
||||
|
**4. Update TestNativeFetcher (`test-apps/.../TestNativeFetcher.java`)**: |
||||
|
- [x] ✅ Change `private volatile String jwtSecret;` → `private volatile String jwtToken;` |
||||
|
- [x] ✅ Update `configure()` method signature and implementation |
||||
|
- [x] ✅ Remove entire `generateJWTToken()` method (HMAC-SHA256 implementation removed) |
||||
|
- [x] ✅ Update `fetchContentWithRetry()` to use `jwtToken` directly |
||||
|
- [x] ✅ Remove JWT-related constants (`JWT_EXPIRATION_MINUTES`) - no longer needed |
||||
|
- [x] ✅ Remove HMAC-SHA256 imports (`Mac`, `SecretKeySpec`, `Base64`, `MessageDigest`) - not used elsewhere |
||||
|
|
||||
|
**5. Update HomeView.vue (`test-apps/.../HomeView.vue`)**: |
||||
|
- [x] ✅ Implement ES256K JWT generation using `did-jwt` and `ethers` libraries |
||||
|
- [x] ✅ Generate JWT before calling `configureNativeFetcher()` using `generateEndorserJWT()` |
||||
|
- [x] ✅ Update `configureNativeFetcher()` call to use `jwtToken` instead of `jwtSecret` |
||||
|
- [x] ✅ Remove `as any` type assertion after plugin rebuild |
||||
|
|
||||
|
**6. ES256K JWT Generation Implementation (`test-apps/.../test-user-zero.ts`)**: |
||||
|
- [x] ✅ Add `did-jwt@^7.0.0` and `ethers@^6.0.0` dependencies |
||||
|
- [x] ✅ Implement `generateEndorserJWT()` function: |
||||
|
- Derives Ethereum private key from seed phrase using `ethers.HDNodeWallet` |
||||
|
- Uses `did-jwt.SimpleSigner` for ES256K signing |
||||
|
- Creates proper ES256K signed JWTs matching TimeSafari's pattern |
||||
|
- [x] ✅ Export `generateEndorserJWT` for use in HomeView.vue |
||||
|
|
||||
|
**6. For TimeSafari Production Integration**: |
||||
|
- [ ] In TimeSafari app, generate JWT using `createEndorserJwtForKey()` with account from SQLite |
||||
|
- [ ] Call `DailyNotification.configureNativeFetcher()` with generated token |
||||
|
- [ ] Implement token refresh logic when tokens expire (60 seconds) |
||||
|
- [ ] Handle token expiration errors and regenerate tokens |
||||
|
|
||||
|
**7. Documentation Updates**: |
||||
|
- [ ] Update `docs/NATIVE_FETCHER_CONFIGURATION.md` to reflect token-based approach |
||||
|
- [ ] Document how to generate tokens in TypeScript using TimeSafari infrastructure |
||||
|
- [ ] Remove references to `jwtSecret` and HMAC-SHA256 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 2. Implement SharedPreferences Persistence for Configuration |
||||
|
|
||||
|
- [x] **Status**: ✅ **COMPLETE** |
||||
|
|
||||
|
**Current State**: ✅ Implemented - `getStarredPlanIds()` and `getLastAcknowledgedJwtId()` use SharedPreferences |
||||
|
|
||||
|
**Location**: `TestNativeFetcher.java` → `getStarredPlanIds()`, `getLastAcknowledgedJwtId()` |
||||
|
|
||||
|
**Implementation Status**: ✅ **COMPLETE** |
||||
|
- ✅ SharedPreferences initialized in constructor with `PREFS_NAME = "DailyNotificationPrefs"` |
||||
|
- ✅ `getStarredPlanIds()` - Loads JSON array from SharedPreferences |
||||
|
- ✅ `getLastAcknowledgedJwtId()` - Loads JWT ID from SharedPreferences |
||||
|
- ✅ `updateStarredPlanIds()` - Saves plan IDs to SharedPreferences |
||||
|
- ✅ `updateLastAckedJwtId()` - Saves JWT ID to SharedPreferences |
||||
|
- ✅ Context passed to constructor and stored as `appContext` |
||||
|
|
||||
|
**Impact**: ✅ Proper state management and pagination support enabled |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 3. Add Error Handling and Retry Logic |
||||
|
|
||||
|
- [x] **Status**: ✅ **COMPLETE** |
||||
|
|
||||
|
**Current State**: ✅ Implemented - `fetchContentWithRetry()` with exponential backoff |
||||
|
|
||||
|
**Location**: `TestNativeFetcher.java` → `fetchContentWithRetry()` |
||||
|
|
||||
|
**Implementation Status**: |
||||
|
- [x] ✅ Retry logic for network failures (transient errors) - `fetchContentWithRetry()` with `MAX_RETRIES = 3` |
||||
|
- [x] ✅ Distinguish retryable vs non-retryable errors - `shouldRetry()` method checks response codes and retry count |
||||
|
- [x] ✅ Log error details for debugging - Detailed logging with retry counts and error messages |
||||
|
- [x] ✅ Exponential backoff for retries - `RETRY_DELAY_MS * (1 << retryCount)` implemented |
||||
|
|
||||
|
**Suggested Implementation**: |
||||
|
```java |
||||
|
private static final int MAX_RETRIES = 3; |
||||
|
private static final int RETRY_DELAY_MS = 1000; |
||||
|
|
||||
|
private CompletableFuture<List<NotificationContent>> fetchContentWithRetry( |
||||
|
FetchContext context, int retryCount) { |
||||
|
// ... existing fetch logic ... |
||||
|
|
||||
|
if (responseCode == 401 || responseCode == 403) { |
||||
|
// Non-retryable - authentication issue |
||||
|
Log.e(TAG, "Authentication failed - check credentials"); |
||||
|
return CompletableFuture.completedFuture(Collections.emptyList()); |
||||
|
} |
||||
|
|
||||
|
if (responseCode >= 500 && retryCount < MAX_RETRIES) { |
||||
|
// Retryable - server error |
||||
|
Log.w(TAG, "Server error " + responseCode + ", retrying... (" + retryCount + "/" + MAX_RETRIES + ")"); |
||||
|
Thread.sleep(RETRY_DELAY_MS * (1 << retryCount)); // Exponential backoff |
||||
|
return fetchContentWithRetry(context, retryCount + 1); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Impact**: Improves reliability for transient network issues |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🟡 Medium Priority |
||||
|
|
||||
|
### 4. Validate API Response Structure (Need to Verify Actual Structure) |
||||
|
|
||||
|
- [x] **Status**: ✅ **VERIFIED** - Endpoint structure verified, parser validation needs improvement |
||||
|
|
||||
|
**Current State**: Parser handles both flat and nested structures, but could add more validation |
||||
|
|
||||
|
**Location**: `TestNativeFetcher.java` → `parseApiResponse()` |
||||
|
|
||||
|
**✅ Endpoint Structure Verified:** |
||||
|
|
||||
|
- [x] ✅ **Check endorser-ch endpoint implementation:** |
||||
|
- [x] ✅ Found `/api/v2/report/plansLastUpdatedBetween` handler in endorser-ch source: `~/projects/timesafari/endorser-ch/src/api/controllers/report-router.js` |
||||
|
- [x] ✅ Verified actual response structure - Returns `{ data: [...], hitLimit: boolean }` where data items have `plan` and `wrappedClaimBefore` properties |
||||
|
- [x] ✅ Response matches `PlanSummaryAndPreviousClaim` structure - Items have `plan.handleId`, `plan.jwtId`, and `wrappedClaimBefore` properties |
||||
|
|
||||
|
- [x] ✅ **Current parser handles:** |
||||
|
- [x] ✅ Both flat structure (`jwtId`, `planId`) and nested structure (`plan.jwtId`, `plan.handleId`) |
||||
|
- [ ] ⚠️ **Missing**: Does NOT extract `wrappedClaimBefore` from response (exists in API response but not used in parser) |
||||
|
|
||||
|
**✅ Verification Results:** |
||||
|
|
||||
|
**Null Checks Status:** |
||||
|
- [x] ✅ Uses `.has()` checks before accessing fields (planId, jwtId) - Lines 502, 511 |
||||
|
- [x] ✅ Checks `item.has("plan")` before accessing plan object - Lines 504, 513 |
||||
|
- [x] ✅ Checks `dataArray != null` after getting it - Line 491 |
||||
|
- [x] ✅ Handles null values safely (planId and jwtId can be null) - Lines 499-518 |
||||
|
- [x] ✅ Wrapped in try-catch for error handling - Line 485, 570 |
||||
|
- [ ] ⚠️ **Missing**: No check for `root.has("data")` before calling `getAsJsonArray("data")` (line 490) - Could throw `IllegalStateException` if "data" field missing or wrong type |
||||
|
- [ ] ⚠️ **Potential Issue**: `getAsJsonObject("plan")` (lines 505, 514) could throw if "plan" exists but is not a JsonObject (currently caught by try-catch) |
||||
|
|
||||
|
**Structure Validation Status:** |
||||
|
- [ ] ⚠️ **Missing**: No validation that root has "data" field before accessing (line 490) |
||||
|
- [ ] ⚠️ **Missing**: No warning logged if "data" field is missing or wrong type (would be caught by try-catch, but no specific warning) |
||||
|
- [x] ✅ Handles missing/null data gracefully (creates default notification if contents empty) - Line 554-568 |
||||
|
- [x] ✅ Handles parse errors gracefully (returns empty list on exception) - Line 570-573 |
||||
|
|
||||
|
**Nested Structure Handling:** |
||||
|
- [x] ✅ Handle nested structures correctly - Parser handles both `item.plan.handleId` and `item.planId` formats |
||||
|
- [x] ✅ Handles both flat (`jwtId`, `planId`) and nested (`plan.jwtId`, `plan.handleId`) structures |
||||
|
|
||||
|
**Remaining Work:** |
||||
|
- [ ] Add `root.has("data")` check before accessing data array (line 490 - currently could throw if "data" missing) |
||||
|
- [ ] Add null check for `plan` object before accessing nested fields (line 505, 514 - currently relies on try-catch) |
||||
|
- [ ] Log warnings for unexpected response formats (missing "data" field, wrong types) |
||||
|
- [ ] Extract `wrappedClaimBefore` from response items (API provides this but parser doesn't use it) |
||||
|
|
||||
|
**Suggested Improvements**: |
||||
|
```java |
||||
|
private List<NotificationContent> parseApiResponse(String responseBody, FetchContext context) { |
||||
|
try { |
||||
|
JsonParser parser = new JsonParser(); |
||||
|
JsonObject root = parser.parse(responseBody).getAsJsonObject(); |
||||
|
|
||||
|
// Validate response structure |
||||
|
if (!root.has("data")) { |
||||
|
Log.w(TAG, "API response missing 'data' field"); |
||||
|
return Collections.emptyList(); |
||||
|
} |
||||
|
|
||||
|
JsonArray dataArray = root.getAsJsonArray("data"); |
||||
|
if (dataArray == null) { |
||||
|
Log.w(TAG, "API response 'data' field is not an array"); |
||||
|
return Collections.emptyList(); |
||||
|
} |
||||
|
|
||||
|
// ... rest of parsing with null checks ... |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Impact**: Prevents crashes on unexpected API responses |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 5. Add Comprehensive Logging for Debugging |
||||
|
|
||||
|
- [ ] **Status**: Partial - Basic logging exists, more comprehensive logging needed |
||||
|
|
||||
|
**Current State**: Basic logging present (Log.d/i/e/w), but could be more detailed and structured |
||||
|
|
||||
|
**Location**: Throughout `TestNativeFetcher.java` |
||||
|
|
||||
|
**Current Implementation**: |
||||
|
- ✅ Uses Log.d/i/e/w for different severity levels |
||||
|
- ✅ Some structured tags (e.g., "TestNativeFetcher") |
||||
|
- ✅ Logs error details and retry information |
||||
|
|
||||
|
**Required Changes**: |
||||
|
- [ ] Add more comprehensive log levels (DEBUG, INFO, WARN, ERROR) - Basic levels exist but could be more systematic |
||||
|
- [ ] Log request/response details (truncated for sensitive data) - Some logging exists, could be more comprehensive |
||||
|
- [ ] Log timing information for performance monitoring - Not currently implemented (timing would be helpful) |
||||
|
- [ ] Add structured logging tags for filtering - Basic tags exist, could be more structured with consistent prefixes |
||||
|
|
||||
|
**Suggested Logging Points**: |
||||
|
- Configuration received |
||||
|
- Fetch start/end with timing |
||||
|
- HTTP request details (URL, method, headers - sanitized) |
||||
|
- Response code and size |
||||
|
- Parse success/failure |
||||
|
- Number of notifications created |
||||
|
|
||||
|
**Example**: |
||||
|
```java |
||||
|
Log.d(TAG, String.format("FETCH_START trigger=%s scheduledTime=%d fetchTime=%d", |
||||
|
context.trigger, context.scheduledTime, context.fetchTime)); |
||||
|
|
||||
|
long startTime = System.currentTimeMillis(); |
||||
|
// ... radius logic ... |
||||
|
long duration = System.currentTimeMillis() - startTime; |
||||
|
|
||||
|
Log.i(TAG, String.format("FETCH_COMPLETE duration=%dms items=%d", |
||||
|
duration, contents.size())); |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 6. Implement Proper Context Management |
||||
|
|
||||
|
- [x] **Status**: ✅ **COMPLETE** |
||||
|
|
||||
|
**Current State**: ✅ Context passed to constructor and stored |
||||
|
|
||||
|
**Location**: `TestNativeFetcher.java` constructor, `TestApplication.java` |
||||
|
|
||||
|
**Implementation Status**: |
||||
|
- [x] ✅ Pass `Application` context to `TestNativeFetcher` constructor - Done in `TestApplication.onCreate()` |
||||
|
- [x] ✅ Store context for SharedPreferences access - Stored as `appContext` and used for SharedPreferences |
||||
|
- [x] ✅ Using application context (no leaks) - `context.getApplicationContext()` used |
||||
|
|
||||
|
**Example**: |
||||
|
```java |
||||
|
// In TestNativeFetcher: |
||||
|
private final Context appContext; |
||||
|
|
||||
|
public TestNativeFetcher(Context context) { |
||||
|
this.appContext = context.getApplicationContext(); // Use app context |
||||
|
// ... initialize SharedPreferences ... |
||||
|
} |
||||
|
|
||||
|
// In TestApplication.onCreate(): |
||||
|
Context context = getApplicationContext(); |
||||
|
TestNativeFetcher fetcher = new TestNativeFetcher(context); |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🟢 Low Priority / Nice to Have |
||||
|
|
||||
|
### 7. Add Unit Tests |
||||
|
|
||||
|
- [ ] **Status**: Not started |
||||
|
|
||||
|
**Location**: Create `TestNativeFetcher/CONTENT_FETCHERTest.java` |
||||
|
|
||||
|
**Coverage Needed**: |
||||
|
- [ ] JWT token generation (verify signature format) |
||||
|
- [ ] API response parsing (valid and invalid responses) |
||||
|
- [ ] Error handling (network failures, invalid responses) |
||||
|
- [ ] Configuration updates |
||||
|
- [ ] SharedPreferences integration |
||||
|
|
||||
|
**Test Framework**: JUnit 4/5 + Mockito |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 8. Extract JWT Generation to Utility Class |
||||
|
|
||||
|
- [ ] **Status**: Not started |
||||
|
|
||||
|
**Current State**: JWT generation code embedded in fetcher |
||||
|
|
||||
|
**Benefit**: Reusable across other parts of the app, easier to test |
||||
|
|
||||
|
**Location**: Create `test-apps/daily-notification-test/android/app/src/main/java/com/timesafari/dailynotification/test/JwtUtils.java` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 9. Add Request/Response Interceptors for Development |
||||
|
|
||||
|
- [ ] **Status**: Not started |
||||
|
|
||||
|
**Purpose**: Log full request/response for API debugging |
||||
|
|
||||
|
**Implementation**: |
||||
|
- [ ] Add methods to log full HTTP requests/responses (with data sanitization) |
||||
|
|
||||
|
**Note**: Only enable in debug builds for security |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 10. Support API Response Caching |
||||
|
|
||||
|
- [ ] **Status**: Not started |
||||
|
|
||||
|
**Current State**: Always fetches from API |
||||
|
|
||||
|
**Enhancement**: |
||||
|
- [ ] Cache successful responses with TTL |
||||
|
- [ ] Use cached data if available and fresh |
||||
|
|
||||
|
**Use Case**: Reduce API calls when multiple notifications scheduled close together |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 11. Add Metrics/Telemetry |
||||
|
|
||||
|
- [ ] **Status**: Not started |
||||
|
|
||||
|
**Purpose**: Track fetch success rates, latency, error types |
||||
|
|
||||
|
**Implementation**: |
||||
|
- [ ] Record metrics to SharedPreferences or analytics service |
||||
|
- [ ] Track: success/failure rate, average response time, error distribution |
||||
|
- [ ] Log summary periodically |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 12. ✅ MOVED TO HIGH PRIORITY: Verify API Response Structure |
||||
|
|
||||
|
**This item has been moved to High Priority item #4** - Validate API Response Structure |
||||
|
|
||||
|
**Action Required** (covered in item #4 above): |
||||
|
- [x] ✅ Check endorser-ch source code for actual endpoint response structure |
||||
|
- [x] ✅ Compare with `PlansLastUpdatedResponse` and `PlanSummaryWithPreviousClaim` interfaces in codebase |
||||
|
- [x] ✅ Update parser if structure differs - Parser handles both flat and nested structures |
||||
|
- [ ] Add validation for required fields - Additional validation still needed |
||||
|
- [ ] Test with real API responses from endorser-ch server - Pending real API testing |
||||
|
|
||||
|
**Location**: |
||||
|
- Check endorser-ch source: `src/` or `server/` directories for endpoint handler |
||||
|
- Check `src/definitions.ts` in this codebase for expected interface structure |
||||
|
- Test with real responses from running endorser-ch server |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📝 Configuration & Setup Tasks |
||||
|
|
||||
|
### 13. Enable Real API Calls in Test Config |
||||
|
|
||||
|
- [ ] **Status**: Ready to enable - ES256K JWT generation complete, network config ready |
||||
|
|
||||
|
**Current State**: `TEST_USER_ZERO_CONFIG.api.serverMode = "mock"` |
||||
|
|
||||
|
**Ready to Enable**: ES256K JWT generation is complete, network security config is in place |
||||
|
|
||||
|
**Action**: |
||||
|
- [ ] Change `serverMode` to `"localhost"` in `test-user-zero.ts` (line 28) for emulator testing |
||||
|
- [ ] Test API calls to `http://10.0.2.2:3000` with real ES256K signed tokens |
||||
|
- [ ] Verify tokens are accepted by endorser-ch API |
||||
|
- [ ] Update to `"staging"` or `"production"` for real environment testing when ready |
||||
|
|
||||
|
**Location**: `test-apps/daily-notification-test/src/config/test-user-zero.ts` (line 28) |
||||
|
|
||||
|
**Note**: JWT generation now uses `generateEndorserJWT()` which creates proper ES256K signed tokens from the seed phrase. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 14. Update TypeScript Type Definitions |
||||
|
|
||||
|
- [x] **Status**: ✅ **VERIFIED** - Type definitions exist and are being used, may need updates after ES256K changes |
||||
|
|
||||
|
**Current State**: Using `as any` type assertion for `configureNativeFetcher` in HomeView.vue |
||||
|
|
||||
|
**Verification Results:** |
||||
|
- [x] ✅ `configureNativeFetcher()` method added to plugin interface (`src/definitions.ts`) - Verified: Line 376 |
||||
|
- [x] ✅ Method signature exists with `apiBaseUrl`, `activeDid`, `jwtSecret` parameters |
||||
|
- [x] ✅ Method is being called in `HomeView.vue` - Line 474 (`onMounted` hook) |
||||
|
- [x] ✅ Currently uses `(DailyNotification as any).configureNativeFetcher` type assertion - Line 454 |
||||
|
|
||||
|
**Remaining Work:** |
||||
|
- [ ] Rebuild plugin package to ensure TypeScript definitions are up to date in `dist/` |
||||
|
- [ ] Remove `as any` type assertion in `HomeView.vue` (after confirming types are available) |
||||
|
- [ ] ⚠️ **CRITICAL**: Update method signature after ES256K implementation (remove `jwtSecret`, add private key mechanism) |
||||
|
|
||||
|
**Location**: `test-apps/daily-notification-test/src/views/HomeView.vue` - Currently uses `(DailyNotification as any).configureNativeFetcher` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 15. Add Network Security Configuration |
||||
|
|
||||
|
- [x] **Status**: ✅ **COMPLETE** - Network security config created and AndroidManifest.xml updated |
||||
|
|
||||
|
**Purpose**: Allow HTTP connections in Android emulator (for localhost:3000) |
||||
|
|
||||
|
**Verification Results:** |
||||
|
- [x] ✅ Checked: `network_security_config.xml` does NOT exist in `android/app/src/main/res/xml/` |
||||
|
- [x] ✅ Checked: No `networkSecurityConfig` attribute in `AndroidManifest.xml` |
||||
|
- [x] ✅ Checked: No `usesCleartextTraffic` attribute in `AndroidManifest.xml` |
||||
|
|
||||
|
**Implementation Status**: |
||||
|
- [x] ✅ Created `android/app/src/main/res/xml/network_security_config.xml` allowing cleartext traffic for `10.0.2.2`, `localhost`, and `127.0.0.1` |
||||
|
- [x] ✅ Added `android:networkSecurityConfig="@xml/network_security_config"` to `<application>` tag in `AndroidManifest.xml` |
||||
|
- [x] ✅ Documented that this is safe for development (emulator-only access) |
||||
|
|
||||
|
**Files Modified**: |
||||
|
- `test-apps/daily-notification-test/android/app/src/main/res/xml/network_security_config.xml` (created) |
||||
|
- `test-apps/daily-notification-test/android/app/src/main/AndroidManifest.xml` (updated line 10) |
||||
|
|
||||
|
**Note**: Required for HTTP connections (not HTTPS) to `http://10.0.2.2:3000` from Android emulator |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🧪 Testing Checklist |
||||
|
|
||||
|
Before considering this implementation complete: |
||||
|
|
||||
|
- [ ] ⚠️ **CRITICAL**: JWT tokens are accepted by endorser API (pending ES256K implementation - current HMAC-SHA256 will fail) |
||||
|
- [ ] API responses are correctly parsed into notifications |
||||
|
- [ ] Notifications appear at scheduled times |
||||
|
- [ ] Prefetch occurs 5 minutes before notification |
||||
|
- [x] ✅ Error handling works for network failures - Retry logic implemented |
||||
|
- [x] ✅ SharedPreferences persists starred plan IDs - Implementation complete |
||||
|
- [ ] Pagination works with `afterId` parameter (SharedPreferences ready, needs API testing) |
||||
|
- [ ] Multiple notifications can be scheduled and fetched |
||||
|
- [ ] Background fetches work when app is closed |
||||
|
- [x] ✅ App recovers gracefully from API errors - Error handling and retry logic implemented |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📚 Related Documentation |
||||
|
|
||||
|
- `docs/NATIVE_FETCHER_CONFIGURATION.md` - Native fetcher configuration guide |
||||
|
- `src/types/content-fetcher.ts` - TypeScript type definitions |
||||
|
- `src/definitions.ts` - `PlansLastUpdatedResponse` and `PlanSummaryWithPreviousClaim` interfaces |
||||
|
- `examples/native-fetcher-android.kt` - Example Android implementation |
||||
|
- **Investigation Results**: `INVESTIGATION_JWT_ALGORITHM_RESULTS.md` - ✅ Complete investigation findings |
||||
|
- [endorser-ch Repository](https://github.com/trentlarson/endorser-ch/tree/master) - **CONFIRMED**: Uses `did-jwt.verifyJWT()` with DID resolver for ES256K verification |
||||
|
- JWT verification: `~/projects/timesafari/endorser-ch/src/api/services/vc/index.js` - `decodeAndVerifyJwt()` |
||||
|
- `/api/v2/report/plansLastUpdatedBetween` endpoint handler: `~/projects/timesafari/endorser-ch/src/api/controllers/report-router.js` |
||||
|
- [did-jwt Library](https://github.com/decentralized-identity/did-jwt) - DID-based JWT implementation (JavaScript reference) |
||||
|
- **TimeSafari Repository**: `~/projects/timesafari/crowd-funder-for-time-pwa/src/libs/crypto/vc/index.ts` |
||||
|
- **CONFIRMED**: Uses `createEndorserJwtForKey()` with `did-jwt.createJWT()` and ES256K signing |
||||
|
- Signs with Ethereum private key from DID identity |
||||
|
- Reference implementation for production JWT generation |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Notes |
||||
|
|
||||
|
- **JWT Implementation**: ✅ **CONFIRMED** - TimeSafari repository investigation complete. Endorser-ch API uses **DID-based JWTs (ES256K)** for API authentication. Current HMAC-SHA256 implementation must be replaced. See `INVESTIGATION_JWT_ALGORITHM_RESULTS.md` for complete findings. |
||||
|
- **Context Dependency**: Consider whether `TestNativeFetcher` should be a singleton or if context should be injected differently |
||||
|
- **Error Strategy**: Decide on error handling strategy - should fetches fail silently (current), or propagate errors for user notification? |
||||
|
- **State Management**: Consider whether starred plan IDs should be managed by the app or fetched from API user profile |
||||
|
- **Investigation Status**: ✅ Item #1 (JWT algorithm) investigation **COMPLETE**. Confirmed ES256K requirement. Implementation changes required before proceeding with other items. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## ✅ Verification Summary (2025-10-31) |
||||
|
|
||||
|
**Items Verified Through Code Inspection:** |
||||
|
|
||||
|
1. **Item #4 (API Response Structure)**: |
||||
|
- ✅ Endpoint structure confirmed in endorser-ch source |
||||
|
- ✅ Parser handles both flat and nested structures |
||||
|
- ✅ Null checks exist for most fields (uses `.has()` checks) |
||||
|
- ⚠️ Missing: `root.has("data")` check before accessing data array |
||||
|
- ⚠️ Missing: `wrappedClaimBefore` extraction from response |
||||
|
|
||||
|
2. **Item #14 (TypeScript Types)**: |
||||
|
- ✅ Method exists in `src/definitions.ts` (Line 376) |
||||
|
- ✅ Method signature includes `apiBaseUrl`, `activeDid`, `jwtSecret` |
||||
|
- ✅ Method being called in `HomeView.vue` (Line 474, `onMounted` hook) |
||||
|
- ⚠️ Currently uses `as any` type assertion (needs plugin rebuild for proper types) |
||||
|
|
||||
|
3. **Item #15 (Network Security Config)**: |
||||
|
- ✅ Confirmed: `network_security_config.xml` does NOT exist |
||||
|
- ✅ Confirmed: No `networkSecurityConfig` attribute in AndroidManifest.xml |
||||
|
- ✅ Confirmed: No `usesCleartextTraffic` attribute in AndroidManifest.xml |
||||
|
- Needs to be created for HTTP connections to `http://10.0.2.2:3000` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Last Updated**: 2025-10-31 (Checkboxes updated - Items 4, 14, 15 verified through code inspection) |
||||
|
**Next Review**: After ES256K JWT signing implementation is complete |
||||
|
|
||||
|
|
||||
@ -0,0 +1,33 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<!-- |
||||
|
Network Security Configuration for Daily Notification Test App |
||||
|
|
||||
|
Allows cleartext HTTP traffic to localhost (10.0.2.2) for Android emulator testing. |
||||
|
This is required for connecting to http://10.0.2.2:3000 from the Android emulator |
||||
|
(which maps to host machine's localhost:3000). |
||||
|
|
||||
|
Author: Matthew Raymer |
||||
|
Date: 2025-10-31 |
||||
|
--> |
||||
|
<network-security-config> |
||||
|
<!-- |
||||
|
Allow cleartext traffic to Android emulator's localhost alias (10.0.2.2). |
||||
|
This is safe because: |
||||
|
1. Only accessible from Android emulator |
||||
|
2. Only used for development/testing |
||||
|
3. Does not affect production builds (production should use HTTPS) |
||||
|
--> |
||||
|
<domain-config cleartextTrafficPermitted="true"> |
||||
|
<!-- Android emulator's special IP for host machine's localhost --> |
||||
|
<domain includeSubdomains="false">10.0.2.2</domain> |
||||
|
<!-- Also allow direct localhost access (for some emulator configurations) --> |
||||
|
<domain includeSubdomains="false">localhost</domain> |
||||
|
<domain includeSubdomains="false">127.0.0.1</domain> |
||||
|
</domain-config> |
||||
|
|
||||
|
<!-- |
||||
|
Production domains should use HTTPS only (default behavior). |
||||
|
This configuration only adds exceptions for localhost development. |
||||
|
--> |
||||
|
</network-security-config> |
||||
|
|
||||
Loading…
Reference in new issue